Extend Nuxeo Drive Series #4 - Customize the Nuxeo Drive hierarchy


Tue 28 May 2013 By Laurent Doguin

Keeping on with the extend Nuxeo Drive series, I'll write about TopLevelFolderItemFactory. Just make sure you read the second post of the series first. The TopLevelFolderItemFactory is used to represent the Nuxeo Drive folder. Its default implementation displays the list of your synchronization root. This is the list of documents where you clicked on the sync icon. You can actually change this to display other things. You could, for instance, display the content of your personal workspace only. To do that we need a new TopLevelFolderItemFactory and the underlying adapter. Let's get to it.

The New Top Level Item Factory


Instead of displaying all of the synchronized roots, we want to display the children of the user's personal workspace. So we need to declare a new TopLevelFolderItemFactory. This is done, as usual, by contributing to an extension point. As there can be only one top level item factory, this contribution will override the previous one, but only if it is deployed after the previous contribution. This is why you see the "require" tag.

<?xml version="1.0"?>
<component name="org.nuxeo.sample.adapters.userworkspace"
version="1.0">
<require>org.nuxeo.drive.adapters</require>
<extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
point="topLevelFolderItemFactory">
<topLevelFolderItemFactory
class="org.nuxeo.sample.UserWorkspaceOnlyTopLevelFactory">
<parameters>
<parameter name="folderName">Nuxeo Drive</parameter>
</parameters>
</topLevelFolderItemFactory>
</extension>
</component>

As you can see, we have a new class called UserWorkspaceOnlyTopLevelFactory. This is our new factory that will return our new adapter. It's pretty much the same one as the default DefaultTopLevelFolderItemFactory, except that the adaptDocument method will return an instance of our new adapter, that isFileSystemItem will verify if the adapted document is indeed a UserWorkspace and the most important change, getTopLevelFolderItem returns the adapted user's personal workspace.

/*

  • (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
    *
  • All rights reserved. This program and the accompanying materials
  • are made available under the terms of the GNU Lesser General Public License
  • (LGPL) version 2.1 which accompanies this distribution, and is available at
  • http://www.gnu.org/licenses/lgpl-2.1.html
    *
  • This library 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
  • Lesser General Public License for more details.
    *
  • Contributors:
  • Antoine Taillefer <[email protected]>
    */
    package org.nuxeo.sample;

import java.security.Principal;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.drive.adapter.FileSystemItem;
import org.nuxeo.drive.adapter.FolderItem;
import org.nuxeo.drive.hierarchy.userworkspace.adapter.UserWorkspaceHelper;
import org.nuxeo.drive.service.FileSystemItemManager;
import org.nuxeo.drive.service.TopLevelFolderItemFactory;
import org.nuxeo.drive.service.impl.AbstractFileSystemItemFactory;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.repository.RepositoryManager;
import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService;
import org.nuxeo.runtime.api.Framework;

/**

  • @author Antoine Taillefer
    */
    public class UserWorkspaceOnlyTopLevelFactory extends AbstractFileSystemItemFactory

     implements TopLevelFolderItemFactory {
    

    private static final Log log = LogFactory.getLog(UserWorkspaceOnlyTopLevelFactory.class);

    protected static final String FOLDER_NAME_PARAM = "folderName";

    protected static final String DEFAULT_FOLDER_NAME = "Nuxeo Drive";

    protected String folderName = DEFAULT_FOLDER_NAME;

    @Override
    public void handleParameters(Map<String, String> parameters)

         throws ClientException {
     // Look for the "folderName" parameter
     String folderNameParam = parameters.get(FOLDER_NAME_PARAM);
     if (!StringUtils.isEmpty(folderNameParam)) {
         folderName = folderNameParam;
     } else {
         log.info(String.format(
                 "Factory %s has no %s parameter, you can provide one in the factory contribution to avoid using the default value '%s'.",
                 getName(), FOLDER_NAME_PARAM, DEFAULT_FOLDER_NAME));
     }
    

    }

    @Override
    public boolean isFileSystemItem(DocumentModel doc, boolean includeDeleted)

         throws ClientException {
     // Check user workspace
     boolean isUserWorkspace = UserWorkspaceHelper.isUserWorkspace(doc);
     if (!isUserWorkspace) {
         log.trace(String.format(
                 "Document %s is not a user workspace, it cannot be adapted as a FileSystemItem.",
                 doc.getId()));
         return false;
     }
     return true;
    

    }

    @Override
    protected FileSystemItem adaptDocument(DocumentModel doc,

         boolean forceParentItem, FolderItem parentItem)
         throws ClientException {
     return new UserWorkspaceOnlyTopLevelFolderItem(getName(), doc, folderName);
    

    }

    @Override
    public FolderItem getVirtualFolderItem(Principal principal)

         throws ClientException {
     return getTopLevelFolderItem(principal);
    

    }

    @Override
    public String getFolderName() {

     return folderName;
    

    }

    @Override
    public void setFolderName(String folderName) {

     this.folderName = folderName;
    

    }

    @Override
    public FolderItem getTopLevelFolderItem(Principal principal)

         throws ClientException {
     DocumentModel userWorkspace = getUserPersonalWorkspace(principal);
     return (FolderItem) getFileSystemItem(userWorkspace);
    

    }

    protected DocumentModel getUserPersonalWorkspace(Principal principal)

         throws ClientException {
     UserWorkspaceService userWorkspaceService = Framework.getLocalService(UserWorkspaceService.class);
     RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
     // TODO: handle multiple repositories
     CoreSession session = getSession(
             repositoryManager.getDefaultRepository().getName(), principal);
     DocumentModel userWorkspace = userWorkspaceService.getCurrentUserPersonalWorkspace(
             session, null);
     if (userWorkspace == null) {
         throw new ClientException(String.format(
                 "No personal workspace found for user %s.",
                 principal.getName()));
     }
     return userWorkspace;
    

    }

    protected CoreSession getSession(String repositoryName, Principal principal)

         throws ClientException {
     return getFileSystemItemManager().getSession(repositoryName, principal);
    

    }

    protected FileSystemItemManager getFileSystemItemManager() {

     return Framework.getLocalService(FileSystemItemManager.class);
    

    }

}

The New Top Level Item Adapter


The goal of this factory is to get the user's personal workspace, and return the appropriate TopLevelFolder adapter, UserWorkspaceOnlyTopLevelFolderItem. Its implementation is rather simple. It extends the default DocumentBackedFolderItem, but overrides some of its methods. The first three methods to override are rename, move and delete, inherited from AbstractFileSystemItem. While our user workspace is indeed a filesystem item, it's also the top level element. So it makes no sense to move, rename or delete it. Let's throw an UnsupportedOperationException instead of the default implementation.

The other method we need to override is the getChildren method. But just be sure to register the UserWorkspace as a synchronization root in case it hasn't been already. The actual getChildren logic is still the one from DocumentBackedFolderItem.

/*

  • (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
    *
  • All rights reserved. This program and the accompanying materials
  • are made available under the terms of the GNU Lesser General Public License
  • (LGPL) version 2.1 which accompanies this distribution, and is available at
  • http://www.gnu.org/licenses/lgpl-2.1.html
    *
  • This library 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
  • Lesser General Public License for more details.
    *
  • Contributors:
  • Antoine Taillefer <[email protected]>
    */
    package org.nuxeo.sample;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.drive.adapter.FileSystemItem;
import org.nuxeo.drive.adapter.FolderItem;
import org.nuxeo.drive.adapter.impl.DocumentBackedFolderItem;
import org.nuxeo.drive.service.NuxeoDriveManager;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.runtime.api.Framework;

/**

  • User workspace only based implementation of the top level [email protected] FolderItem}.
  • <p>
  • Implements the following tree:
    *
  • <pre>
  • Nuxeo Drive
  • |-- User workspace child 1
  • |-- User workspace child 2
  • |-- ...
  • </pre>
    *
  • @author Antoine Taillefer
    */
    public class UserWorkspaceOnlyTopLevelFolderItem extends

     DocumentBackedFolderItem {
    

    private static final long serialVersionUID = 1L;

    private static final Log log = LogFactory.getLog(UserWorkspaceOnlyTopLevelFolderItem.class);

    protected DocumentModel userWorkspace;

    public UserWorkspaceOnlyTopLevelFolderItem(String factoryName,

         DocumentModel userWorkspace, String folderName)
         throws ClientException {
     super(factoryName, null, userWorkspace);
     name = folderName;
     canRename = false;
     canDelete = false;
     this.userWorkspace = userWorkspace;
    

    }

    protected UserWorkspaceOnlyTopLevelFolderItem() {

     // Needed for JSON deserialization
    

    }

    @Override
    public void rename(String name) throws ClientException {

     throw new UnsupportedOperationException(
             "Cannot rename the top level folder item.");
    

    }

    @Override
    public void delete() throws ClientException {

     throw new UnsupportedOperationException(
             "Cannot delete the top level folder item.");
    

    }

    @Override
    public FileSystemItem move(FolderItem dest) throws ClientException {

     throw new UnsupportedOperationException(
             "Cannot move the top level folder item.");
    

    }

    @Override
    public List<FileSystemItem> getChildren() throws ClientException {

     // Register user workspace as a synchronization root if it is not
     // already the case
     if (!getNuxeoDriveManager().isSynchronizationRoot(principal,
             userWorkspace)) {
         getNuxeoDriveManager().registerSynchronizationRoot(principal,
                 userWorkspace, getSession());
     }
     // let DocumentBackedFolderItem handle the getChildren like usual
     return super.getChildren();
    

    }

    protected NuxeoDriveManager getNuxeoDriveManager() {

     return Framework.getLocalService(NuxeoDriveManager.class);
    

    }
    }


My Synchronized Personal workspace

Now our Nuxeo Drive folder should contain only the children of our User Workspace. But you can still synchronize other documents. And it will actually work. It works because when you sync a document, you add the DriveSynchronized facet to it. And there is still an active factory for those. So now you should be wondering why they still show up on the desktop while our UserWorkspaceOnlyTopLevelFolderItem only returns the children of the personal workspace.

There is a reasonable explaination for that. Adding the facet triggers an event, which will be retrieved by your Nuxeo Drive client. Instead of asking for the top level item adapter chilren (which could be costly), it calls the getParentItem of the newly synced document's adapter (the default is DefaultSyncRootFolderItemFactory). As you can see, its implementation returns the top level folder:

    protected FolderItem getParentItem(DocumentModel doc) throws ClientException {
FileSystemItemManager fileSystemItemManager = Framework.getLocalService(FileSystemItemManager.class);
Principal principal = doc.getCoreSession().getPrincipal();
return fileSystemItemManager.getTopLevelFolder(principal);
}

Hence Drive considering it as a child of our top level folder. We have different solutions to avoid this issue. We could disable the defaultSyncRootFolderItemFactory or remove all the Drive Sync icons. Removing the icons seems to be the best solution. We don't want users to click on the icon and then see nothing happening in their Nuxeo Drive folder. To disable the icons, we have to override the actions contribution:

<?xml version="1.0" encoding="UTF-8"?>
<component name="org.nuxeo.drive.actions.hierarchy.userworkspace">
<require>org.nuxeo.drive.actions</require>
<extension target="org.nuxeo.ecm.platform.actions.ActionService"
point="actions">
<action id="driveSynchronizeCurrentDocument" enabled="false" />
<action id="driveUnsynchronizeCurrentDocument" enabled="false" />
<action id="driveNavigateToCurrentSynchronizationRoot" enabled="false" />
</extension>
</component>

With this configuration, your users will only see the content of their personal workspace. And they won't be able to add any other synchronization roots.


Category: Product & Development
Tagged: How to, Java, Nuxeo Drive