SpacesRestService.java

/*
 * Copyright (C) 2003-2010 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.social.service.rest;

import static org.exoplatform.social.service.rest.RestChecker.checkAuthenticatedRequest;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.*;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.commons.collections.map.HashedMap;

import org.apache.commons.lang3.StringUtils;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.commons.utils.Safe;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.rest.resource.ResourceContainer;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.social.core.identity.model.Identity;
import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
import org.exoplatform.social.core.manager.IdentityManager;
import org.exoplatform.social.core.service.LinkProvider;
import org.exoplatform.social.core.space.SpaceException;
import org.exoplatform.social.core.space.SpaceFilter;
import org.exoplatform.social.core.space.model.Space;
import org.exoplatform.social.core.space.spi.SpaceService;
import org.exoplatform.social.rest.impl.space.SpaceRestResourcesV1;
import org.exoplatform.social.rest.impl.user.UserRestResourcesV1;
import org.exoplatform.social.service.rest.api.models.IdentityNameList;
import org.exoplatform.social.service.rest.api.models.IdentityNameList.Option;
import org.exoplatform.web.WebAppController;
import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.metadata.ControllerDescriptor;
import org.exoplatform.web.controller.metadata.DescriptorBuilder;
import org.exoplatform.web.controller.router.Router;
import org.exoplatform.web.controller.router.RouterConfigException;
import org.exoplatform.web.controller.router.URIWriter;

/**
 *
 * Provides services for the space gadget to display a user's spaces and pending spaces.
 *
 * @anchor SpacesRestService
 *
 */
@Path("{portalName}/social/spaces")
public class SpacesRestService implements ResourceContainer {
  private static final Log LOG = ExoLogger.getLogger(SpacesRestService.class);
  private SpaceService _spaceService;
  private IdentityManager _identityManager;
  /**
   * Confirmed Status information
   */
  private static final String CONFIRMED_STATUS = "confirmed";
  /**
   * Pending Status information
   */
  private static final String PENDING_STATUS = "pending";
  /**
   * Incoming Status information
   */
  private static final String INCOMING_STATUS = "incoming";
  /**
   * Public Status information
   */
  private static final String ALL_SPACES_STATUS = "all_spaces";

  private String portalContainerName;


  /**
   * Qualified name path for rendering url.
   * 
   * @since 1.2.2
   */
  private static final QualifiedName PATH = QualifiedName.create("gtn", "path");

  /**
   * Qualified name path for rendering url.
   * 
   * @since 1.2.9
   */
  private static final QualifiedName LANG = QualifiedName.create("gtn", "lang");
  
  /**
   * Qualified name site type for rendering url.
   * 
   * @since 1.2.2
   */
  private static final QualifiedName REQUEST_SITE_TYPE = QualifiedName.create("gtn", "sitetype");
  
  /**
   * Qualified name handler for rendering url.
   * 
   * @since 1.2.2
   */
  private static final QualifiedName REQUEST_HANDLER = QualifiedName.create("gtn", "handler");
  
  /**
   * Qualified name site name for rendering url.
   * 
   * @since 1.2.2
   */
  private static final QualifiedName REQUEST_SITE_NAME = QualifiedName.create("gtn", "sitename");
  
  private static final String ALL_SPACES = "all-spaces";
  
  private static final String JSON = "json";
  
  /**
   * constructor
   */
  public SpacesRestService() {
  }

  /**
   * Gets the current user's spaces and pending spaces.
   *
   * @param uriInfo The requested URI information.
   * @param portalName The name of the current container.
   * @param format The format of the returned result.
   * 
   * @anchor SpacesRestService.showMySpaceList
   *
   * @return response
   * 
   * @throws Exception
   * 
   * @LevelAPI Platform
   * @deprecated Deprecated from 4.3.x. Replaced by a new API {@link UserRestResourcesV1#getSpacesOfUser(UriInfo, String, int, int, boolean, String)}
   */
  @GET
  @Path("mySpaces/show.{format}")
  public Response showMySpaceList(@Context UriInfo uriInfo,
                                  @PathParam("portalName") String portalName,
                                  @PathParam("format") String format) throws Exception {
    MediaType mediaType = Util.getMediaType(format);
    ConversationState state = ConversationState.getCurrent();
    portalContainerName = portalName;
    
    String userId = null;
    if (state != null) {
      userId = state.getIdentity().getUserId();
    } 
    
    Identity identity = getIdentityManager().getOrCreateIdentity(OrganizationIdentityProvider.NAME, userId, false);
    if (identity == null) {
      userId = Util.getViewerId(uriInfo);
    }
    
    SpaceList mySpaceList = showMySpaceList(userId);
    
    this.fillUrlAllSpaces(mySpaceList, portalName);
    return Util.getResponse(mySpaceList, uriInfo, mediaType, Response.Status.OK);
  }
  
 
  /**
   * Provides a way to get the latest spaces ordered by last access and to be able to filter spaces, based on the application Id in the spaces.
   *
   *
   * @param uriInfo The requested URI information.
   * @param portalName The portal container name.
   * @param format The format of the returned result, for example, JSON, or XML.
   * @param offset Specifies the staring point of the returned results. It must be greater than or equal to 0.
   * @param limit Specifies the ending point of the returned results. It must be less than or equal to 10.
   * @param appId The application Id which is contained in spaces to filter, such as, Wiki, Discussion, Documents, Agenda and more.
   * @authentication
   * @request GET: {@code http://localhost:8080/rest/private/social/spaces/lastVisitedSpace/list.json?appId=Wiki&offset=0&limit=10}
   * @response
   * {
   * "spaces":[
   *        {"groupId":"/spaces/space_2","spaceUrl":null,"name":"space_2","displayName":"space 2","url":"space_2"},
   *        {"groupId":"/spaces/space_1","spaceUrl":null,"name":"space_1","displayName":"space 1","url":"space_1"}
   *       ],
   * "moreSpacesUrl":null
   * }
   * @return the response
   * @LevelAPI Platform
   * @anchor SpacesRestService.getLastVisitedSpace
   *
   */
  @GET
  @Path("lastVisitedSpace/list.{format}")
  public Response getLastVisitedSpace(@Context UriInfo uriInfo,
                                  @PathParam("portalName") String portalName,
                                  @PathParam("format") String format,
                                  @QueryParam("appId") String appId,
                                  @QueryParam("offset") int offset,
                                  @QueryParam("limit") int limit) throws Exception {
    checkAuthenticatedRequest();
    
    MediaType mediaType = Util.getMediaType(format, new String[]{format});
    ConversationState state = ConversationState.getCurrent();
    portalContainerName = portalName;
    
    String userId = null;
    if (state != null) {
      userId = state.getIdentity().getUserId();
    } 
    
    Identity identity = getIdentityManager().getOrCreateIdentity(OrganizationIdentityProvider.NAME, userId, false);
    if (identity == null) {
      userId = Util.getViewerId(uriInfo);
    }
    
    //
    int newLimit = Math.min(limit, 100);
    int newOffset = 0;
    if (offset > 0) {
      newOffset = Math.min(offset, newLimit);
    } else {
      newOffset = 0;
    }
    
    //
    String newAppId = null;
    if (appId != null && appId.trim().length() > 0) {
      newAppId = appId;
    }
    
    SpaceList mySpaceList = getLastVisitedSpace(userId, newAppId, newOffset, newLimit);
    return Util.getResponse(mySpaceList, uriInfo, mediaType, Response.Status.OK);
  }
  
  /**
   * Gets space display info
   *
   * @param uriInfo The requested URI information.
   * @authentication
   * @request GET: {@code http://localhost:8080/rest/private/social/spaces/spaceInfo/?spaceName=space1}
   * @response
   * {
   * {"displayName":"space 2","url":"","imageSource":""},
   * }
   * @return the response
   * @LevelAPI Platform
   * @anchor SpacesRestService.getSpaceInfo
   *
   */
  @GET
  @Path("spaceInfo")
  public Response getSpaceInfo(@Context UriInfo uriInfo,
          @PathParam("portalName") String portalName,
          @QueryParam("spaceName") String spaceName) throws Exception {
    checkAuthenticatedRequest();
    
    MediaType mediaType = Util.getMediaType(JSON, new String[]{JSON});
    portalContainerName = portalName;
    
    Space space = getSpaceService().getSpaceByPrettyName(spaceName);
    
    Map<String, String> spaceInfo = new HashMap<>();
    spaceInfo.put("displayName", space.getDisplayName());
    //spaceInfo.put("url", LinkProvider.getSpaceUri(space.getUrl()));
    spaceInfo.put("url", buildSpaceUrl(space.getUrl()));
    
    spaceInfo.put("imageSource", space.getAvatarUrl() != null ? space.getAvatarUrl() :
                                 LinkProvider.SPACE_DEFAULT_AVATAR_URL);
    
    return Util.getResponse(spaceInfo, uriInfo, mediaType, Response.Status.OK);
  }
  
  private String buildSpaceUrl(final String spaceUrl) {
      //http://localhost:8080/portal/g/:spaces:space1/space1
      return "/" + portalContainerName + "/g/:spaces:" +
                   spaceUrl + "/" + spaceUrl;
  }
  
  /**
   * Gets a user's pending spaces.
   * 
   * @param uriInfo The requested URI information.
   * @param portalName The portal container name.
   * @param format The format of the returned result, for example, JSON, or XML.
   * 
   * @anchor SpacesRestService.showPendingSpaceList
   *
   * @return response
   * 
   * @throws Exception
   * 
   * @LevelAPI Platform
   */
  @GET
  @Path("pendingSpaces/show.{format}")
  public Response showPendingSpaceList(@Context UriInfo uriInfo,
                                       @PathParam("portalName") String portalName,
                                       @PathParam("format") String format) throws Exception {
    MediaType mediaType = Util.getMediaType(format);
    String userId = ConversationState.getCurrent().getIdentity().getUserId();
    portalContainerName = portalName;
    if (!userId.equals(Util.getViewerId(uriInfo))) {
      return null;
    }
    SpaceList pendingSpaceList = showPendingSpaceList(userId);
    return Util.getResponse(pendingSpaceList, uriInfo, mediaType, Response.Status.OK);
  }

  /**
   * Suggests the space's name for searching.
   *
   * @param uriInfo The requested URI information.
   * @param portalName The name of portal.
   * @param conditionToSearch The input information to search.
   * @param typeOfRelation The type of relationship of the user and the space.
   * @param userId The Id of current user.
   * @param format The format of the returned result, for example, JSON, or XML.
   * @return
   * @throws Exception
   * @LevelAPI Platform
   * @anchor SpacesRestService.suggestSpacenames
   * @deprecated Deprecated from 4.3.x. Replaced by a new API {@link SpaceRestResourcesV1#getSpaces(UriInfo, Request, String, int, int, String, String, boolean, String)}
   */
  @GET
  @RolesAllowed("users")
  @Path("suggest.{format}")
  public Response suggestSpacenames(@Context UriInfo uriInfo,
                                    @Context HttpServletRequest request,
                                    @PathParam("portalName") String portalName,
                                    @QueryParam("conditionToSearch") String conditionToSearch,
                                    @QueryParam("typeOfRelation") String typeOfRelation,
                                    @QueryParam("currentUser") String userId,
                                    @PathParam("format") String format) throws Exception {

    if(StringUtils.isBlank(userId)) {
      throw new WebApplicationException(Response.Status.BAD_REQUEST);
    }
    if(!userId.equals(request.getRemoteUser())) {
      throw new WebApplicationException(Response.Status.UNAUTHORIZED);
    }

    MediaType mediaType = Util.getMediaType(format);
    portalContainerName = portalName;
    SpaceService spaceSrv = getSpaceService();

    IdentityNameList nameList = new IdentityNameList();

    
      if (ALL_SPACES_STATUS.equals(typeOfRelation)) {
        ListAccess<Space> listAccess = spaceSrv.getAccessibleSpacesByFilter(userId, new SpaceFilter(conditionToSearch));
        List<Space> spaces = Arrays.asList(listAccess.load(0, 10));
        addSpaceNames(nameList, spaces);
      } else {
        if (PENDING_STATUS.equals(typeOfRelation)) {
          ListAccess<Space> listAccess = spaceSrv.getPendingSpacesByFilter(userId, new SpaceFilter(conditionToSearch));
          List<Space> spaces = Arrays.asList(listAccess.load(0, 10));
          addSpaceNames(nameList, spaces);
        } else if (INCOMING_STATUS.equals(typeOfRelation)) {
          ListAccess<Space> listAccess = spaceSrv.getInvitedSpacesByFilter(userId, new SpaceFilter(conditionToSearch));
          List<Space> spaces = Arrays.asList(listAccess.load(0, 10));
          addSpaceNames(nameList, spaces);
        } else if (CONFIRMED_STATUS.equals(typeOfRelation)) {
          ListAccess<Space> listAccess = spaceSrv.getMemberSpacesByFilter(userId, new SpaceFilter(conditionToSearch));
          List<Space> spaces = Arrays.asList(listAccess.load(0, 10));
          addSpaceNames(nameList, spaces);
        }
    }

    return Util.getResponse(nameList, uriInfo, mediaType, Response.Status.OK);
  }

  private void addSpaceNames(IdentityNameList nameList, List<Space> spaces) {
    int i = 1;
    for (Space space : spaces) {
      Option opt = new Option();
      opt.setType("space");
      opt.setInvalid(false);
      opt.setOrder(i++);
      opt.setText(space.getDisplayName());
      opt.setValue(space.getPrettyName());
      opt.setAvatarUrl(space.getAvatarUrl());
      nameList.addOption(opt);
    }
  }

  /**
   * List that contains space from space service.<br> Need this class for converter from rest
   * service.
   */
  @XmlRootElement
  static public class SpaceList {
    private String moreSpacesUrl;
    
    private List<SpaceRest> _spaces;

    /**
     * sets space list
     *
     * @param spaces space list
     */
    public void setSpaces(List<SpaceRest> spaces) {
      _spaces = spaces;
    }

    /**
     * gets space list
     *
     * @return space list
     */
    public List<SpaceRest> getSpaces() {
      return _spaces;
    }

    /**
     * adds space to space list
     *
     * @param space
     * @see Space
     */
    public void addSpace(SpaceRest space) {
      if (_spaces == null) {
        _spaces = new LinkedList<SpaceRest>();
      }
      _spaces.add(space);
    }
    
    /**
     * Get the url of all spaces.
     * 
     * @return
     * @since 1.2.9
     */
    public String getMoreSpacesUrl() {
      return moreSpacesUrl;
    }
    
    /**
     * Set the url of all spaces.
     * 
     * @param allSpacesUrl
     * @since 1.2.9
     */
    public void setMoreSpacesUrl(String allSpacesUrl) {
      moreSpacesUrl = allSpacesUrl;
    }
  }

  /**
   * shows my spaceList by userId
   *
   * @param userId
   * @return spaceList
   * @see SpaceList
   */
  private SpaceList showMySpaceList(String userId) {
    SpaceList spaceList = new SpaceList();
    _spaceService = getSpaceService();
    List<Space> mySpaces = null;
    List<SpaceRest> mySpacesRest = new ArrayList<SpaceRest>();
    try {
      mySpaces = _spaceService.getSpaces(userId);
      
      for (Space space : mySpaces) {
        SpaceRest spaceRest = new SpaceRest(space);
        mySpacesRest.add(spaceRest);
      }
      
      //fix for issue SOC-2039, sets the space url with new navigation controller
      Router router = this.getRouter(this.getConfigurationPath());
      
      this.fillSpacesURI(mySpacesRest, router);
    } catch (SpaceException e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    } catch (Exception e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }
    
    spaceList.setSpaces(mySpacesRest);
    return spaceList;
  }
  
  /**
   * get my spaceList by userId which user is last visited
   * 
   * @param userId
   * @param appId
   * @param limit
   * @return
   */
  private SpaceList getLastVisitedSpace(String userId, String appId, int offset, int limit) {
    SpaceList spaceList = new SpaceList();
    _spaceService = getSpaceService();
    List<Space> mySpaces = null;
    
    
    try {
      mySpaces = _spaceService.getLastAccessedSpace(userId, appId, offset, limit);
      SpaceRest spaceRest;
      for (Space space : mySpaces) {
        spaceRest = new SpaceRest(space);
        spaceList.addSpace(spaceRest);
      }
    } catch (SpaceException e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    } catch (Exception e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }
    return spaceList;
  }

  /**
   * shows pending spaceList by userId
   *
   * @param userId
   * @return spaceList
   * @see SpaceList
   */
  private SpaceList showPendingSpaceList(String userId) {
    SpaceList spaceList = new SpaceList();
    _spaceService = getSpaceService();
    List<Space> pendingSpaces;
    List<SpaceRest> pendingSpacesRest = new ArrayList<SpaceRest>();
    try {
      pendingSpaces = _spaceService.getPendingSpaces(userId);
      for (Space space : pendingSpaces) {
        SpaceRest spaceRest = new SpaceRest(space);
        pendingSpacesRest.add(spaceRest);
      }
    } catch (SpaceException e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }
    spaceList.setSpaces(pendingSpacesRest);
    return spaceList;
  }

  /**
   * Fill url for more spaces.
   * 
   * @param spaceList
   * @param portalOwner
   * @since 1.2.9
   */
  private void fillUrlAllSpaces(SpaceList spaceList, String portalOwner) {
    try {
      Router router = this.getRouter(this.getConfigurationPath());
      
      Map<QualifiedName, String> qualifiedName = new HashedMap();
      qualifiedName.put(REQUEST_HANDLER, "portal");
      qualifiedName.put(REQUEST_SITE_TYPE, "portal");
      qualifiedName.put(LANG, "");
      
      StringBuilder urlBuilder = new StringBuilder();
      qualifiedName.put(REQUEST_SITE_NAME, portalOwner);
      qualifiedName.put(PATH, ALL_SPACES);
      router.render(qualifiedName, new URIWriter(urlBuilder));
      spaceList.setMoreSpacesUrl(urlBuilder.toString());
    } catch (Exception e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }
  }
  
  /**
   * gets spaceService
   *
   * @return spaceService
   * @see SpaceService
   */
  private SpaceService getSpaceService() {
    return (SpaceService) getPortalContainer().getComponentInstanceOfType(SpaceService.class);
  }

  /**
   * gets identityManager
   *
   * @return
   */
  private IdentityManager getIdentityManager() {
    if (_identityManager == null) {
      _identityManager = (IdentityManager) getPortalContainer().getComponentInstanceOfType(IdentityManager.class);
    }
    return _identityManager;
  }

  private PortalContainer getPortalContainer() {
    return (PortalContainer) ExoContainerContext.getContainerByName(portalContainerName);
  }

  /**
   * Fills the spaces uri.
   * 
   * @param mySpaces
   * @param router
   * @since 1.2.2
   */
  @SuppressWarnings("unchecked")
  private void fillSpacesURI(List<SpaceRest> mySpaces, Router router) {
    try {
      Map<QualifiedName, String> qualifiedName = new HashedMap ();
      qualifiedName.put(REQUEST_HANDLER, "portal");
      qualifiedName.put(REQUEST_SITE_TYPE, "group");
      
      for (SpaceRest space : mySpaces) {
        StringBuilder urlBuilder = new StringBuilder();
        qualifiedName.put(REQUEST_SITE_NAME, space.getGroupId());
        qualifiedName.put(PATH, space.getUrl());
        router.render(qualifiedName, new URIWriter(urlBuilder));
        space.setSpaceUrl(urlBuilder.toString());
      }
    } catch (Exception e) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }
  }
  
  /**
   * Gets the configuration path of file controller.xml
   * 
   * @return
   * @since 1.2.2
   */
  private String getConfigurationPath() {
    PortalContainer portalContainer= this.getPortalContainer();
    WebAppController webAppController = (WebAppController) portalContainer.getComponentInstanceOfType(WebAppController.class);
    return webAppController.getConfigurationPath();
  }
  
  /**
   * Gets the router from path of file controller.xml
   * 
   * @param path
   * @return
   * @throws IOException
   * @throws RouterConfigException
   * @since 1.2.2
   */
  private Router getRouter(String path) throws IOException, RouterConfigException {
     File f = new File(path);
     if (!f.exists()) {
        throw new MalformedURLException("Could not resolve path " + path);
     }
     if (!f.isFile()) {
        throw new MalformedURLException("Could not resolve path " + path + " to a valid file");
     }
     return this.getRouter(f.toURI().toURL());
  }
  
  /**
   * Gets the router from url.
   * 
   * @param url
   * @return
   * @throws RouterConfigException
   * @throws IOException
   * @since 1.2.2
   */
  private Router getRouter(URL url) throws RouterConfigException, IOException {
     InputStream in = url.openStream();
     try {
        ControllerDescriptor routerDesc = new DescriptorBuilder().build(in);
        return new Router(routerDesc);
     } finally {
        Safe.close(in);
     }
  }
}