UIDocumentSelector.java

/*
 * Copyright (C) 2003-2017 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.plugin.doc.selector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.NodeIterator;

import org.apache.commons.lang.StringUtils;

import org.exoplatform.ecm.connector.fckeditor.FCKUtils;
import org.exoplatform.ecm.webui.utils.Utils;
import org.exoplatform.portal.webui.util.Util;
import org.exoplatform.services.cms.BasePath;
import org.exoplatform.services.cms.drives.DriveData;
import org.exoplatform.services.cms.drives.ManageDriveService;
import org.exoplatform.services.cms.drives.impl.ManageDriveServiceImpl;
import org.exoplatform.services.cms.link.LinkManager;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.wcm.core.NodeLocation;
import org.exoplatform.services.wcm.core.NodetypeConstant;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;
import org.exoplatform.social.core.space.SpaceUtils;
import org.exoplatform.social.core.space.model.Space;
import org.exoplatform.social.core.space.spi.SpaceService;
import org.exoplatform.social.plugin.doc.ComposerFileItem;
import org.exoplatform.social.plugin.doc.UIAbstractSelectFileComposer;
import org.exoplatform.social.plugin.doc.UIDocActivityPopup;
import org.exoplatform.social.webui.composer.UIComposer.PostContext;
import org.exoplatform.web.application.RequestContext;
import org.exoplatform.webui.application.WebuiRequestContext;
import org.exoplatform.webui.config.annotation.ComponentConfig;
import org.exoplatform.webui.config.annotation.EventConfig;
import org.exoplatform.webui.core.UIComponent;
import org.exoplatform.webui.cssfile.CssClassManager;
import org.exoplatform.webui.cssfile.CssClassUtils;
import org.exoplatform.webui.event.Event;
import org.exoplatform.webui.event.EventListener;

@ComponentConfig(
  template = "classpath:groovy/social/plugin/doc/selector/UIDocumentSelector.gtmpl",
  events = {
    @EventConfig(listeners = UIDocumentSelector.SelectActionListener.class)
  }
)
public class UIDocumentSelector extends UIAbstractSelectFileComposer {

  public static final String  ACTIVITY_PARAMS_SEPARATOR     = "/@/";

  public static final String  DOCUMENTS_BREADCRUMB_SELECTOR = "DocumentsBreadcumbSelector";

  private static final String ALL_GROUP_PERMISSION          = "*:${groupId}";

  private static final String ALL_USER_PERMISSION           = "${userId}";

  private static final String UPLOAD_RESOLVER_TYPE          = "JCR";

  private static final Log    LOG                           = ExoLogger.getLogger(UIDocumentSelector.class.getName());

  protected String            documentSelectorTitle;

  private ManageDriveService  driveService;

  private SpaceService        spaceService;

  private LinkManager         linkManager;

  private List<Object>        breadCrumb                    = new ArrayList<>();

  private List<String>        breadCrumbTitle               = new ArrayList<>();

  private List<String>        selectedFilePaths             = new ArrayList<>();

  private List<String>        selectedFileTitles             = new ArrayList<>();

  private List<String>        validSelectedFilePaths        = new ArrayList<>();

  private String              lastSelectedDocumentTitle     = null;

  private boolean             documentAlreadySelectedError  = false;

  public UIDocumentSelector() throws Exception {
    super();
    driveService = getApplicationComponent(ManageDriveService.class);
    spaceService = getApplicationComponent(SpaceService.class);
    linkManager = getApplicationComponent(LinkManager.class);

    Iterator<DriveData> drives = getDrives().iterator();

    DriveData selectedDriveData = null;
    try {
      if (StringUtils.isBlank(SpaceUtils.getSpaceUrlByContext())) {
        while (drives.hasNext() && selectedDriveData == null) {
          DriveData driveData = (DriveData) drives.next();
          if (driveData.getName().equals(ManageDriveServiceImpl.PERSONAL_DRIVE_NAME)) {
            selectedDriveData = driveData;
          }
        }
      } else {
        Space space = spaceService.getSpaceByUrl(SpaceUtils.getSpaceUrlByContext());
        while (drives.hasNext() && selectedDriveData == null) {
          DriveData driveData = (DriveData) drives.next();
          if (driveData.getHomePath().contains(space.getGroupId())) {
            selectedDriveData = driveData;
          }
        }
      }
    } catch (Exception e) {
      LOG.warn("Error while initializing Popup", e);
    }

    if (selectedDriveData != null) {
      breadCrumb.clear();
      breadCrumbTitle.clear();
      breadCrumb.add(selectedDriveData);
      breadCrumbTitle.add(getDriveTitle(selectedDriveData));
    }

    addChild(UIDocumentSelectorUpdate.class, null, null);
  }

  public String getLastSelectedDocumentTitle() {
    return lastSelectedDocumentTitle;
  }

  public boolean isDocumentAlreadySelectedError() {
    return documentAlreadySelectedError;
  }

  public void setDocumentAlreadySelectedError(boolean documentAlreadySelectedError) {
    this.documentAlreadySelectedError = documentAlreadySelectedError;
  }

  public void setTitle(String documentSelectorTitle) {
    this.documentSelectorTitle = documentSelectorTitle;
  }

  public String getTitle() {
    return documentSelectorTitle;
  }

  public List<DriveData> getDrives() throws Exception {
    if (breadCrumb.isEmpty()) {
      List<DriveData> driveDatas = new ArrayList<>();
      driveDatas.addAll(driveService.getMainDrives(Util.getPortalRequestContext().getRemoteUser(), Utils.getMemberships()));
      driveDatas.addAll(driveService.getGroupDrives(Util.getPortalRequestContext().getRemoteUser(), Utils.getMemberships()));
      driveDatas.addAll(driveService.getPersonalDrives(Util.getPortalRequestContext().getRemoteUser()));
      return driveDatas;
    } else {
      return Collections.emptyList();
    }
  }

  public List<Node> getFolders() throws Exception {
    if (breadCrumb.isEmpty()) {
      return Collections.emptyList();
    } else {
      DriveData drive = (DriveData) breadCrumb.get(0);
      String path = null;

      if (breadCrumb.size() == 1) {
        path = getDriveHomePath(drive);
      } else {
        path = (String) breadCrumb.get(breadCrumb.size() - 1);
      }

      Node docNode = NodeLocation.getNodeByExpression(WCMCoreUtils.getRepository().getConfiguration().getName() + ":"
          + drive.getWorkspace() + ":" + path);
      List<Node> folderNodes = new ArrayList<>();

      NodeIterator nodesIterator = docNode.getNodes();
      while (nodesIterator.hasNext()) {
        Node node = (Node) nodesIterator.nextNode();
        if (node.isNodeType(FCKUtils.EXO_HIDDENABLE))
          continue;
        if (node.isNodeType("exo:symlink") && node.hasProperty("exo:uuid") && node.hasProperty("exo:workspace")) {
          node = linkManager.getTarget(node);
        }
        if (node.isNodeType(NodetypeConstant.NT_FOLDER) || node.isNodeType(NodetypeConstant.NT_UNSTRUCTURED)) {
          folderNodes.add(node);
        }
      }
      return folderNodes;
    }
  }

  public List<Node> getFiles() throws Exception {
    if (breadCrumb.isEmpty()) {
      return Collections.emptyList();
    } else {
      DriveData drive = (DriveData) breadCrumb.get(0);

      String path = null;

      if (breadCrumb.size() == 1) {
        path = getDriveHomePath(drive);
      } else {
        path = (String) breadCrumb.get(breadCrumb.size() - 1);
      }

      Node docNode = NodeLocation.getNodeByExpression(WCMCoreUtils.getRepository().getConfiguration().getName() + ":"
          + drive.getWorkspace() + ":" + path);
      List<Node> fileNodes = new ArrayList<>();

      NodeIterator nodesIterator = docNode.getNodes();
      while (nodesIterator.hasNext()) {
        Node node = (Node) nodesIterator.nextNode();
        if (node.isNodeType(FCKUtils.EXO_HIDDENABLE))
          continue;
        if (node.isNodeType("exo:symlink") && node.hasProperty("exo:uuid") && node.hasProperty("exo:workspace")) {
          node = linkManager.getTarget(node);
        }
        if (node.isNodeType(NodetypeConstant.NT_FILE) && !fileNodes.contains(node)) {
          fileNodes.add(node);
        }
      }
      return fileNodes;
    }
  }

  public String getDriveTitle(DriveData drive) throws Exception {
    String name = drive.getName();
    if (name.startsWith(".")) {
      String groupLabel = getGroupLabel(drive);
      if (groupLabel == null) {
        groupLabel = getGroupLabel(name, !name.startsWith("/spaces"));
      }
      return groupLabel;
    } else {
      return getLabel(name);
    }
  }

  public String getDriveCSSClasses(DriveData drive) throws Exception {
    if (drive.getName().startsWith(".")) {
      return "uiIconEcms24x24Drive" + drive.getName().replaceAll(" ", "") + " uiIconEcms24x24DriveGroup";
    } else if (drive.getHomePath().contains("${userId}")) {
      return "uiIconEcms24x24Drive" + drive.getName().replaceAll(" ", "") + " uiIconEcms24x24DrivePrivate";
    } else {
      return "uiIconEcms24x24Drive" + drive.getName().replaceAll(" ", "") + " uiIconEcms24x24DriveGeneral";
    }
  }

  public String getLabel(String id) {
    RequestContext context = RequestContext.getCurrentInstance();
    ResourceBundle res = context.getApplicationResourceBundle();
    try {
      String userDisplayName = "";
      if (ManageDriveServiceImpl.USER_DRIVE_NAME.equals(id)) {
        RequestContext ctx = Util.getPortalRequestContext();
        if (ctx != null) {
          String username = ctx.getRemoteUser();
          try {
            User user = this.getApplicationComponent(OrganizationService.class).getUserHandler().findUserByName(username);
            if (user != null) {
              userDisplayName = user.getDisplayName();
            }
          } catch (Exception ex) {
            userDisplayName = username;
          }
        }
      }
      return res.getString("Drives.label." + id.replace(" ", "")).replace("{0}", userDisplayName);
    } catch (Exception ex) {
      return id;
    }
  }

  public String getGroupLabel(DriveData driveData) throws Exception {
    try {
      RepositoryService repoService = WCMCoreUtils.getService(RepositoryService.class);
      NodeHierarchyCreator nodeHierarchyCreator = WCMCoreUtils.getService(NodeHierarchyCreator.class);
      String groupPath = nodeHierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH);
      String absPath = groupPath + driveData.getName().replace(".", "/");
      ManageableRepository currentRepository = repoService.getCurrentRepository();
      String workspace = currentRepository.getConfiguration().getDefaultWorkspaceName();

      return getNode(workspace, absPath).getProperty(NodetypeConstant.EXO_LABEL).getString();
    } catch (Exception e) {
      return null;
    }
  }

  public Node getNode(String workspace, String absPath) throws Exception {
    RepositoryService repoService = WCMCoreUtils.getService(RepositoryService.class);
    ManageableRepository currentRepository = repoService.getCurrentRepository();
    Node groupNode = (Node) WCMCoreUtils.getSystemSessionProvider().getSession(workspace, currentRepository).getItem(absPath);
    return groupNode;
  }

  public String getGroupLabel(String groupId, boolean isFull) {
    String ret = groupId.replace(".", " / ");
    if (!isFull) {
      if (ret.startsWith(" / spaces")) {
        return ret.substring(ret.lastIndexOf("/") + 1).trim();
      }
      int count = 0;
      int slashPosition = -1;
      for (int i = 0; i < ret.length(); i++) {
        if ('/' == ret.charAt(i)) {
          if (++count == 4) {
            slashPosition = i;
            break;
          }
        }
      }
      if (slashPosition > 0) {
        ret = ret.substring(0, slashPosition) + "...";
      } else if (ret.length() > 70) {
        ret = ret.substring(0, 70) + "...";
      }
    }
    return ret;
  }

  public String getFileTitle(Node fileNode) throws Exception {
    return Utils.getTitle(fileNode);
  }

  public String getFilePath(Node fileNode) throws Exception {
    return fileNode.getSession().getWorkspace().getName() + "@" + fileNode.getPath();
  }

  public String getBreadCrumbTitle(Object element) throws Exception {
    if (element instanceof DriveData) {
      return getDriveTitle((DriveData) element);
    } else {
      return breadCrumbTitle.get(breadCrumb.indexOf(element));
    }
  }

  public String getFolderNodeIcon(Node node) throws Exception {
    return Utils.getNodeTypeIcon(node, "uiIcon24x24");
  }

  public double getFileSize(Node node) throws Exception {
    // get file size
    if (node.hasNode(Utils.JCR_CONTENT)) {
      Node contentNode = node.getNode(Utils.JCR_CONTENT);
      if (contentNode.hasProperty(Utils.JCR_DATA)) {
        return contentNode.getProperty(Utils.JCR_DATA).getLength();
      }
    }
    return 0;
  }

  public String getFileTypeCSSClass(Node fileNode) throws Exception {
    String cssClass = CssClassUtils
                                   .getCSSClassByFileNameAndFileType(fileNode.getName(),
                                                                     getFileMimeType(fileNode),
                                                                     CssClassManager.ICON_SIZE.ICON_64)
                                   .replace("uiIcon", "uiBgd");
    return cssClass;
  }

  public static String getFileMimeType(Node node) throws Exception {
    if (node.getPrimaryNodeType().getName().equals(NodetypeConstant.NT_FILE)) {
      if (node.hasNode(NodetypeConstant.JCR_CONTENT))
        return node.getNode(NodetypeConstant.JCR_CONTENT).getProperty(NodetypeConstant.JCR_MIME_TYPE).getString();
    }
    return "";
  }

  public boolean isFileSelected(Node fileNode) throws Exception {
    return breadCrumb.size() > 0 && breadCrumb.get(0) != null
        && StringUtils.equals(((DriveData) breadCrumb.get(0)).getWorkspace(), fileNode.getSession().getWorkspace().getName())
        && selectedFilePaths.contains(getFilePath(fileNode));
  }

  public List<Object> getBreadCrumb() {
    return breadCrumb;
  }

  public String getFolderTitle(Node folderNode) throws Exception {
    return Utils.getTitle(folderNode);
  }

  public static class SelectActionListener extends EventListener<UIDocumentSelector> {

    public void execute(Event<UIDocumentSelector> event) throws Exception {
      UIDocumentSelector component = event.getSource();

      UIComponent componentToUpdate = component;

      String selectedElement = event.getRequestContext().getRequestParameter(OBJECTID);
      String[] params = selectedElement.split(ACTIVITY_PARAMS_SEPARATOR);
      if (params.length < 3) {
        LOG.warn("Number of parameters must be 3 or greater (separated by /@/ ): {} ", selectedElement);
        return;
      }

      String selectionType = params[0];
      String elementType = params[1];
      String selectedPath = params[2];
      if (selectionType.equals("BREADCRUMB")) {
        if (StringUtils.isBlank(selectedPath)) {
          LOG.warn("No entry was selected: {}", selectedElement);
          return;
        } else if (elementType.equals("DRIVES")) {
          component.breadCrumb.clear();
          component.breadCrumbTitle.clear();
        } else {
          int breadCrumbIndex = Integer.parseInt(selectedPath);
          if (elementType.equals("DRIVE")) {
            if (breadCrumbIndex != 0) {
              LOG.warn("Selected index isn't a drive: {} ", breadCrumbIndex);
              return;
            }
            component.breadCrumb = component.getBreadCrumb().subList(0, 1);
            component.breadCrumbTitle = component.breadCrumbTitle.subList(0, 1);
          } else if (elementType.equals("FOLDER")) {
            if (component.breadCrumb.size() < 2) {
              LOG.warn("No folder was selected: {}", breadCrumbIndex);
              return;
            }
            component.breadCrumb = component.breadCrumb.subList(0, breadCrumbIndex + 1);
            component.breadCrumbTitle = component.breadCrumbTitle.subList(0, breadCrumbIndex + 1);
          } else {
            LOG.warn("Invalid breadcrumb element type: {} ", elementType);
            return;
          }
        }
      } else if (selectionType.equals("SELECTIONBOX")) {
        if (elementType.equals("DRIVE")) {
          if (component.getBreadCrumb().size() > 0) {
            LOG.warn("Can't switch to another Drive while a drive is already selected: {} ", selectedPath);
            return;
          }
          DriveData driveData = component.driveService.getDriveByName(selectedPath);
          if (driveData != null && component.hasPermissionOnDrive(driveData)) {
            component.breadCrumb.clear();
            component.breadCrumbTitle.clear();
            component.breadCrumb.add(driveData);
            component.breadCrumbTitle.add(component.getDriveTitle(driveData));
          } else {
            LOG.warn("Can't find drive with name {}", selectedPath);
            return;
          }
        } else if (elementType.equals("FOLDER")) {
          if (component.getBreadCrumb().size() == 0) {
            LOG.warn("Can't find the selected drive for selected folder: {} ", selectedPath);
            return;
          }
          DriveData driveData = (DriveData) component.getBreadCrumb().get(0);
          component.breadCrumb.add(selectedPath);
          component.breadCrumbTitle.add(component.getFolderTitle(component.getNode(driveData.getWorkspace(), selectedPath)));
        } else if (elementType.equals("FILE")) {
          UIDocActivityPopup docActivityPopup = component.getAncestorOfType(UIDocActivityPopup.class);
          componentToUpdate = component.getChild(UIDocumentSelectorUpdate.class);

          if (params.length != 4) {
            LOG.warn("Number of parameters must be equal to 4 (separated by /@/ ): {} ", selectedElement);
            return;
          }
          String selectedFileTitle = params[3];

          if (component.selectedFilePaths.contains(selectedPath)) {
            component.selectedFilePaths.remove(selectedPath);
            component.selectedFileTitles.remove(selectedFileTitle);
            docActivityPopup.setLimitReached(component.selectedFilePaths.size() >= docActivityPopup.getMaxFilesCount());
          } else {
            component.lastSelectedDocumentTitle = selectedFileTitle;

            if(component.selectedFileTitles.contains(selectedFileTitle)) {
              component.documentAlreadySelectedError = true;
            } else {
              docActivityPopup.setLimitReached(component.selectedFilePaths.size() >= docActivityPopup.getMaxFilesCount());
              if (!docActivityPopup.isLimitReached()) {
                component.selectedFilePaths.add(selectedPath);
                if (StringUtils.isNotBlank(selectedFileTitle)) {
                  component.selectedFileTitles.add(selectedFileTitle);
                }
              }
            }

          }
        }
      } else {
        LOG.warn("Invalid selection type: {} ", selectionType);
        return;
      }

      event.getRequestContext().addUIComponentToUpdateByAjax(componentToUpdate);
    }
  }

  protected String getDriveHomePath(DriveData driveData) throws Exception {
    String homePath = driveData.getHomePath();
    if (homePath.contains("${userId}")) {
      homePath = org.exoplatform.services.cms.impl.Utils.getPersonalDrivePath(homePath,
                                                                              Util.getPortalRequestContext().getRemoteUser());
    }
    return homePath;
  }

  protected boolean hasPermissionOnDrive(DriveData drive) throws Exception {
    List<String> userMemberships = Utils.getMemberships();
    String[] allPermission = drive.getAllPermissions();
    if (ALL_GROUP_PERMISSION.equals(allPermission[0]) || ALL_USER_PERMISSION.equals(allPermission[0])) {
      return true;
    }
    for (String membership : userMemberships) {
      if (drive.hasPermission(allPermission, membership)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void validateSelection() {
    validSelectedFilePaths.clear();
    validSelectedFilePaths.addAll(selectedFilePaths);
    selectedFilePaths.clear();
    selectedFileTitles.clear();
  }

  public boolean hasSelectedFiles() {
    return !selectedFilePaths.isEmpty();
  }

  @Override
  public Set<ComposerFileItem> getSelectFiles() {
    if (validSelectedFilePaths == null || validSelectedFilePaths.isEmpty()) {
      return Collections.emptySet();
    }
    Set<ComposerFileItem> composerFileItems = new HashSet<>();
    for (String selectedFile : validSelectedFilePaths) {
      try {
        String selectedFileWS = selectedFile.substring(0, selectedFile.indexOf("@"));
        String selectedFilePath = selectedFile.substring(selectedFile.indexOf("@") + 1);

        Node node = getNode(selectedFileWS, selectedFilePath);

        ComposerFileItem composerFileItem = new ComposerFileItem();
        composerFileItem.setName(node.getName());
        composerFileItem.setMimeType(getFileMimeType(node));
        composerFileItem.setId(getFilePath(node));
        composerFileItem.setTitle(getFileTitle(node));
        double fileSize = getFileSize(node);

        String mbString = "MB";
        try {
          ResourceBundle resourceBundle = WebuiRequestContext.getCurrentInstance().getApplicationResourceBundle();
          mbString = resourceBundle.getString("UIComposerDocumentExtension.label.megabyte");
        } catch (Exception e) {
        }
        String fileSizeMB = (((int) (Math.abs(fileSize / (1024 * 1024)) * 100)) / 100d) + " " + mbString;

        composerFileItem.setSizeInBytes(fileSize);
        composerFileItem.setSize(fileSizeMB);
        composerFileItem.setNodeIcon(getFileTypeCSSClass(node));
        composerFileItem.setResolverType("JCR");

        composerFileItems.add(composerFileItem);
      } catch (Exception e) {
        LOG.warn("Error occurred while proceeding selection of file " + selectedFile, e);
      }
    }
    return composerFileItems;
  }

  @Override
  public String getResolverType() {
    return UPLOAD_RESOLVER_TYPE;
  }

  @Override
  public Object preActivitySave(Object resource, PostContext postContext) throws Exception {
    ComposerFileItem fileItem = (ComposerFileItem) resource;
    String path = fileItem.getId();
    String selectedFileWS = path.substring(0, path.indexOf("@"));
    String selectedFilePath = path.substring(path.indexOf("@") + 1);
    return getNode(selectedFileWS, selectedFilePath);
  }

  @Override
  public void resetSelection() {
    breadCrumb.clear();
    breadCrumbTitle.clear();
    selectedFilePaths.clear();
    validSelectedFilePaths.clear();
    selectedFileTitles.clear();
  }

  @Override
  protected void removeSelectedFile(ComposerFileItem fileItem) {
    // Nothing to do
  }
}