ome.services.blitz.repo.ManagedRepositoryI.java Source code

Java tutorial

Introduction

Here is the source code for ome.services.blitz.repo.ManagedRepositoryI.java

Source

/*
 * Copyright (C) 2012-2014 Glencoe Software, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package ome.services.blitz.repo;

import static omero.rtypes.rstring;

import java.io.IOException;
import java.text.DateFormatSymbols;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import loci.formats.FormatReader;

import ome.api.IAdmin;
import ome.api.IUpdate;
import ome.formats.importer.ImportConfig;
import ome.formats.importer.ImportContainer;
import ome.model.core.OriginalFile;
import ome.model.meta.Experimenter;
import ome.services.blitz.gateway.services.util.ServiceUtilities;
import ome.services.blitz.repo.path.ClientFilePathTransformer;
import ome.services.blitz.repo.path.FilePathNamingValidator;
import ome.services.blitz.repo.path.FilePathRestrictionInstance;
import ome.services.blitz.repo.path.FsFile;
import ome.services.blitz.util.ChecksumAlgorithmMapper;
import ome.system.Roles;
import ome.system.ServiceFactory;
import ome.util.SqlAction;
import ome.util.checksum.ChecksumProviderFactory;
import ome.util.checksum.ChecksumProviderFactoryImpl;
import omero.ResourceError;
import omero.ServerError;
import omero.grid.ImportLocation;
import omero.grid.ImportProcessPrx;
import omero.grid.ImportSettings;
import omero.grid._ManagedRepositoryOperations;
import omero.grid._ManagedRepositoryTie;
import omero.model.ChecksumAlgorithm;
import omero.model.Fileset;
import omero.model.FilesetEntry;
import omero.model.FilesetI;
import omero.model.FilesetJobLink;
import omero.model.FilesetVersionInfo;
import omero.model.FilesetVersionInfoI;
import omero.model.IndexingJobI;
import omero.model.Job;
import omero.model.MetadataImportJob;
import omero.model.MetadataImportJobI;
import omero.model.PixelDataJobI;
import omero.model.ThumbnailGenerationJob;
import omero.model.ThumbnailGenerationJobI;
import omero.model.UploadJob;
import omero.sys.EventContext;
import omero.util.IceMapper;

import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.lang.text.StrSubstitutor;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import Ice.Current;

/**
 * Extension of the PublicRepository API which only manages files
 * under ${omero.data.dir}/ManagedRepository.
 *
 * @author Colin Blackburn <cblackburn at dundee dot ac dot uk>
 * @author Josh Moore, josh at glencoesoftware.com
 * @author m.t.b.carroll@dundee.ac.uk
 * @since 5.0
 */
public class ManagedRepositoryI extends PublicRepositoryI implements _ManagedRepositoryOperations {

    private final static Logger log = LoggerFactory.getLogger(ManagedRepositoryI.class);

    private final static int parentDirsToRetain = 3;

    /* This class is used in the server-side creation of import containers.
     * The suggestImportPaths method sanitizes the paths in due course.
     * From the server side, we cannot imitate ImportLibrary.createImport
     * in applying client-side specifics to clean up the path. */
    private static final ClientFilePathTransformer nopClientTransformer = new ClientFilePathTransformer(
            new Function<String, String>() {
                // @Override  since JDK6
                public String apply(String from) {
                    return from;
                }
            });

    /* used for generating %monthname% for path templates */
    private static final DateFormatSymbols DATE_FORMAT = new DateFormatSymbols();

    /* referenced by only the bare-bones ManagedRepositoryI constructor used in testing */
    private static final String ALL_CHECKSUM_ALGORITHMS = Joiner.on(',').join(Collections2.transform(
            ChecksumAlgorithmMapper.getAllChecksumAlgorithms(), ChecksumAlgorithmMapper.CHECKSUM_ALGORITHM_NAMER));

    private final FilePathNamingValidator filePathNamingValidator;

    private final String template;

    private final ProcessContainer processes;

    private final String rootSessionUuid;

    private final long userGroupId;

    /**
     * Creates a {@link ProcessContainer} internally that will not be managed
     * by background threads. Used primarily during testing.
     * @param template
     * @param dao
     */
    public ManagedRepositoryI(String template, RepositoryDao dao) throws Exception {
        this(template, dao, new ProcessContainer(), new ChecksumProviderFactoryImpl(), ALL_CHECKSUM_ALGORITHMS,
                FilePathRestrictionInstance.UNIX_REQUIRED.name, null, new Roles());
    }

    public ManagedRepositoryI(String template, RepositoryDao dao, ProcessContainer processes,
            ChecksumProviderFactory checksumProviderFactory, String checksumAlgorithmSupported, String pathRules,
            String rootSessionUuid, Roles roles) throws ServerError {
        super(dao, checksumProviderFactory, checksumAlgorithmSupported, pathRules);
        this.template = template;
        this.processes = processes;
        this.filePathNamingValidator = new FilePathNamingValidator(this.filePathRestrictions);
        this.rootSessionUuid = rootSessionUuid;
        this.userGroupId = roles.getUserGroupId();
        log.info("Repository template: " + this.template);
    }

    @Override
    public Ice.Object tie() {
        return new _ManagedRepositoryTie(this);
    }

    /**
     * Split a template path into the root- and user-owned segments
     * @param templatePath a template path
     * @return the root- and user-owned segments
     * @throws ApiUsageException if there are no user-owned components
     */
    private static Map.Entry<FsFile, FsFile> splitPath(String templatePath) throws omero.ApiUsageException {
        int splitPoint = templatePath.lastIndexOf("//");
        if (splitPoint < 0) {
            splitPoint = 0;
        }

        final FsFile rootPath = new FsFile(templatePath.substring(0, splitPoint));
        final FsFile userPath = new FsFile(templatePath.substring(splitPoint));

        if (FsFile.emptyPath.equals(userPath)) {
            throw new omero.ApiUsageException(null, null,
                    "no user-owned directories in managed repository template path");
        }

        return Maps.immutableEntry(rootPath, userPath);
    }

    //
    // INTERFACE METHODS
    //

    /**
     * Return a template based directory path. The path will be created
     * by calling {@link #makeDir(String, boolean, Ice.Current)}.
     */
    public ImportProcessPrx uploadFileset(Fileset fs, ImportSettings settings, Ice.Current __current)
            throws omero.ServerError {

        ImportLocation location = internalImport(fs, settings, __current);
        return createUploadProcess(fs, location, settings, __current, true);

    }

    /**
     * Return a template based directory path. The path will be created
     * by calling {@link #makeDir(String, boolean, Ice.Current)}.
     */
    public ImportProcessPrx importFileset(Fileset fs, ImportSettings settings, Ice.Current __current)
            throws omero.ServerError {

        ImportLocation location = internalImport(fs, settings, __current);
        return createImportProcess(fs, location, settings, __current);

    }

    private ImportLocation internalImport(Fileset fs, ImportSettings settings, Ice.Current __current)
            throws omero.ServerError {

        if (fs == null || fs.sizeOfUsedFiles() < 1) {
            throw new omero.ApiUsageException(null, null, "No paths provided");
        }

        if (settings == null) {
            settings = new ImportSettings(); // All defaults.
        }
        if (settings.checksumAlgorithm == null) {
            throw new omero.ApiUsageException(null, null, "must specify checksum algorithm");
        }

        final List<FsFile> paths = new ArrayList<FsFile>();
        for (FilesetEntry entry : fs.copyUsedFiles()) {
            paths.add(new FsFile(entry.getClientPath().getValue()));
        }

        // This is the first part of the string which comes after:
        // ManagedRepository/, e.g. %user%/%year%/etc.
        final EventContext ec = repositoryDao.getEventContext(__current);
        final String templatePath = expandTemplate(template, ec);
        final Map.Entry<FsFile, FsFile> pathSegments = splitPath(templatePath);
        final FsFile rootOwnedPath = pathSegments.getKey();
        final FsFile userOwnedPath = pathSegments.getValue();

        // at this point, the template path should not yet exist on the filesystem
        createTemplateDir(rootOwnedPath, userOwnedPath, __current);

        final FsFile relPath = FsFile.concatenate(rootOwnedPath, userOwnedPath);
        fs.setTemplatePrefix(rstring(relPath.toString() + FsFile.separatorChar));

        final Class<? extends FormatReader> readerClass = getReaderClass(fs, __current);

        // The next part of the string which is chosen by the user:
        // /home/bob/myStuff
        FsFile basePath = commonRoot(paths);

        // If any two files clash in that chosen basePath directory, then
        // we want to suggest a similar alternative.
        return suggestImportPaths(relPath, basePath, paths, readerClass, settings.checksumAlgorithm, __current);

    }

    public ImportProcessPrx importPaths(List<String> paths, Ice.Current __current) throws ServerError {

        if (paths == null || paths.size() < 1) {
            throw new omero.ApiUsageException(null, null, "No paths provided");
        }

        final ImportContainer container = new ImportContainer(null /*file*/, null /*target*/, null /*userPixels*/,
                "Unknown" /*reader*/, paths.toArray(new String[0]), false /*spw*/);

        final ImportSettings settings = new ImportSettings();
        final Fileset fs = new FilesetI();
        try {
            container.fillData(new ImportConfig(), settings, fs, nopClientTransformer);
            settings.checksumAlgorithm = this.checksumAlgorithms.get(0);
        } catch (IOException e) {
            // impossible
            ServiceUtilities.handleException(e, "IO exception from operation without IO");
        }

        return importFileset(fs, settings, __current);
    }

    public List<ImportProcessPrx> listImports(Ice.Current __current) throws omero.ServerError {

        final List<Long> filesetIds = new ArrayList<Long>();
        final List<ImportProcessPrx> proxies = new ArrayList<ImportProcessPrx>();
        final EventContext ec = repositoryDao.getEventContext(__current);
        final List<ProcessContainer.Process> ps = processes.listProcesses(ec.memberOfGroups);

        for (final ProcessContainer.Process p : ps) {
            filesetIds.add(p.getFileset().getId().getValue());
        }

        final List<Fileset> filesets = repositoryDao.loadFilesets(filesetIds, __current);

        for (Fileset fs : filesets) {
            if (!fs.getDetails().getPermissions().canEdit()) {
                filesetIds.remove(fs.getId().getValue());
            }
        }

        for (final ProcessContainer.Process p : ps) {
            if (filesetIds.contains(p.getFileset().getId())) {
                proxies.add(p.getProxy());
            }
        }

        return proxies;
    }

    public List<ChecksumAlgorithm> listChecksumAlgorithms(Current __current) {
        return this.checksumAlgorithms;
    }

    public ChecksumAlgorithm suggestChecksumAlgorithm(List<ChecksumAlgorithm> supported, Current __current) {
        final Set<String> supportedNames = new HashSet<String>(
                Lists.transform(supported, ChecksumAlgorithmMapper.CHECKSUM_ALGORITHM_NAMER));
        for (final ChecksumAlgorithm configured : listChecksumAlgorithms(__current)) {
            if (supportedNames.contains(ChecksumAlgorithmMapper.CHECKSUM_ALGORITHM_NAMER.apply(configured))) {
                return configured;
            }
        }
        return null;
    }

    //
    // HELPERS
    //

    /**
     * Creating the process will register itself in an appropriate
     * container (i.e. a SessionI or similar) for the current
     * user and therefore this instance no longer needs to worry
     * about the maintenance of the object.
     */
    protected ImportProcessPrx createImportProcess(Fileset fs, ImportLocation location, ImportSettings settings,
            Current __current) throws ServerError {

        // Initialization version info
        final ImportConfig config = new ImportConfig();
        final FilesetVersionInfo serverVersionInfo = new FilesetVersionInfoI();
        serverVersionInfo.setBioformatsReader(rstring("Unknown"));
        config.fillVersionInfo(serverVersionInfo);

        // Create and validate jobs
        if (fs.sizeOfJobLinks() != 1) {
            throw new omero.ValidationException(null, null,
                    "Found more than one job link. " + "Link only updateJob on creation!");
        }
        final FilesetJobLink jobLink = fs.getFilesetJobLink(0);
        final Job job = jobLink.getChild();
        if (job == null) {
            throw new omero.ValidationException(null, null, "Found null-UploadJob on creation");
        }
        if (!(job instanceof UploadJob)) {
            throw new omero.ValidationException(null, null,
                    "Found non-UploadJob on creation: " + job.getClass().getName());
        }

        MetadataImportJob metadata = new MetadataImportJobI();
        metadata.setVersionInfo(serverVersionInfo);
        fs.linkJob(metadata);

        fs.linkJob(new PixelDataJobI());

        if (settings.doThumbnails != null && settings.doThumbnails.getValue()) {
            ThumbnailGenerationJob thumbnail = new ThumbnailGenerationJobI();
            fs.linkJob(thumbnail);
        }

        fs.linkJob(new IndexingJobI());

        if (location instanceof ManagedImportLocationI) {
            OriginalFile of = ((ManagedImportLocationI) location).getLogFile().asOriginalFile(IMPORT_LOG_MIMETYPE);
            of = persistLogFile(of, __current);
            job.linkOriginalFile((omero.model.OriginalFile) new IceMapper().map(of));
        }

        return createUploadProcess(fs, location, settings, __current, false);
    }

    /**
     * Creating the process will register itself in an appropriate
     * container (i.e. a SessionI or similar) for the current
     * user and therefore this instance no longer needs to worry
     * about the maintenance of the object.
     */
    protected ImportProcessPrx createUploadProcess(Fileset fs, ImportLocation location, ImportSettings settings,
            Current __current, boolean uploadOnly) throws ServerError {

        // Create CheckedPath objects for use by saveFileset
        final int size = fs.sizeOfUsedFiles();
        final List<CheckedPath> checked = new ArrayList<CheckedPath>();
        for (int i = 0; i < size; i++) {
            final String path = location.sharedPath + FsFile.separatorChar + location.usedFiles.get(i);
            checked.add(checkPath(path, settings.checksumAlgorithm, __current));
        }

        final Fileset managedFs = repositoryDao.saveFileset(getRepoUuid(), fs, settings.checksumAlgorithm, checked,
                __current);
        // Since the fileset saved validly, we create a session for the user
        // and return the process.

        final ManagedImportProcessI proc = new ManagedImportProcessI(this, managedFs, location, settings,
                __current);
        processes.addProcess(proc);
        return proc.getProxy();
    }

    /**
     * Get the suggested BioFormats Reader for the given Fileset.
     * @param fs a fileset
     * @param __current the current ICE context
     * @return a reader class, or null if none could be found
     */
    protected Class<? extends FormatReader> getReaderClass(Fileset fs, Current __current) {
        for (final Job job : fs.linkedJobList()) {
            if (job instanceof UploadJob) {
                Class<?> potentialReaderClass;
                try {
                    final FilesetVersionInfo versionInfo = ((UploadJob) job).getVersionInfo(__current);
                    final String readerName = versionInfo.getBioformatsReader(__current).getValue();
                    potentialReaderClass = Class.forName(readerName);
                } catch (NullPointerException npe) {
                    log.debug("No info provided for reader class");
                    continue;
                } catch (Exception e) {
                    log.warn("Error getting reader class", e);
                    continue;
                }
                if (FormatReader.class.isAssignableFrom(potentialReaderClass)) {
                    return (Class<? extends FormatReader>) potentialReaderClass;
                }
            }
        }
        return null;
    }

    /**
     * From a list of paths, calculate the common root path that all share. In
     * the worst case, that may be "/". May not include the last element, the filename.
     * @param some paths
     * @return the paths' common root
     */
    protected FsFile commonRoot(List<FsFile> paths) {
        final List<String> commonRoot = new ArrayList<String>();
        int index = 0;
        boolean isCommon = false;
        while (true) {
            String component = null;
            for (final FsFile path : paths) {
                final List<String> components = path.getComponents();
                if (components.size() <= index + 1) // prohibit very last component
                    isCommon = false; // not long enough
                else if (component == null) {
                    component = components.get(index);
                    isCommon = true; // first path
                } else // subsequent long-enough path
                    isCommon = component.equals(components.get(index));
                if (!isCommon)
                    break;
            }
            if (isCommon)
                commonRoot.add(paths.get(0).getComponents().get(index++));
            else
                break;
        }
        return new FsFile(commonRoot);
    }

    /**
     * Turn the current template into a relative path. Makes use of the data
     * returned by {@link #replacementMap(Ice.Current)}.
     *
     * @param curr
     * @return
     */
    protected String expandTemplate(final String template, EventContext ec) {

        if (template == null) {
            return ""; // EARLY EXIT.
        }

        final Map<String, String> map = replacementMap(ec);
        final StrSubstitutor strSubstitutor = new StrSubstitutor(new StrLookup() {
            @Override
            public String lookup(final String key) {
                return map.get(key);
            }
        }, "%", "%", '%');
        return strSubstitutor.replace(template);
    }

    /**
     * Generates a map with most of the fields (as strings) from the
     * {@link EventContext} for the current user as well as fields from
     * a current {@link Calendar} instance. Implementors need only
     * provide the fields that are used in their templates. Any keys that
     * cannot be found by {@link #expandeTemplate(String, Ice.Current)} will
     * remain untouched.
     *
     * @param curr
     * @return
     */
    protected Map<String, String> replacementMap(EventContext ec) {
        final Map<String, String> map = new HashMap<String, String>();
        final Calendar now = Calendar.getInstance();
        map.put("user", ec.userName);
        map.put("userId", Long.toString(ec.userId));
        map.put("group", ec.groupName);
        map.put("groupId", Long.toString(ec.groupId));
        map.put("year", Integer.toString(now.get(Calendar.YEAR)));
        map.put("month", String.format("%02d", now.get(Calendar.MONTH) + 1));
        map.put("monthname", DATE_FORMAT.getMonths()[now.get(Calendar.MONTH)]);
        map.put("day", String.format("%02d", now.get(Calendar.DAY_OF_MONTH)));
        map.put("time", String.format("%02d-%02d-%02d.%03d", now.get(Calendar.HOUR_OF_DAY),
                now.get(Calendar.MINUTE), now.get(Calendar.SECOND), now.get(Calendar.MILLISECOND)));
        map.put("session", ec.sessionUuid);
        map.put("sessionId", Long.toString(ec.sessionId));
        map.put("eventId", Long.toString(ec.eventId));
        map.put("perms", ec.groupPermissions.toString());
        return map;
    }

    /**
     * Take the relative path created by
     * {@link #expandTemplate(String, Ice.Current)} and call
     * {@link makeDir(String, boolean, Ice.Current)} on each element of the path
     * starting at the top, until all the directories have been created.
     * The full path must not already exist, although a prefix of it may.
     */
    protected void createTemplateDir(FsFile rootOwnedPath, FsFile userOwnedPath, Ice.Current curr)
            throws ServerError {
        if (!rootOwnedPath.getComponents().isEmpty()) {
            final Current rootCurr = sudo(curr, rootSessionUuid);
            rootCurr.ctx.put(omero.constants.GROUP.value, Long.toString(userGroupId));
            makeDir(rootOwnedPath.toString(), true, rootCurr);
        }

        final FsFile relPath = FsFile.concatenate(rootOwnedPath, userOwnedPath);

        if (userOwnedPath.getComponents().size() > 1) {
            final int relPathSize = relPath.getComponents().size();
            final List<String> relPathPrefix = relPath.getComponents().subList(0, relPathSize - 1);
            makeDir(new FsFile(relPathPrefix).toString(), true, curr);
        }
        makeDir(relPath.toString(), false, curr);
    }

    /** Return value for {@link #trimPaths}. */
    private static class Paths {
        final FsFile basePath;
        final List<FsFile> fullPaths;

        Paths(FsFile basePath, List<FsFile> fullPaths) {
            this.basePath = basePath;
            this.fullPaths = fullPaths;
        }
    }

    /**
     * Trim off the start of long client-side paths.
     * @param basePath the common root
     * @param fullPaths the full paths from the common root down to the filename
     * @param readerClass BioFormats reader for data, may be null
     * @return possibly trimmed common root and full paths
     */
    protected Paths trimPaths(FsFile basePath, List<FsFile> fullPaths, Class<? extends FormatReader> readerClass) {
        // find how many common parent directories to retain according to BioFormats
        Integer commonParentDirsToRetain = null;
        final String[] localStylePaths = new String[fullPaths.size()];
        int index = 0;
        for (final FsFile fsFile : fullPaths)
            localStylePaths[index++] = serverPaths.getServerFileFromFsFile(fsFile).getAbsolutePath();
        try {
            commonParentDirsToRetain = readerClass.newInstance().getRequiredDirectories(localStylePaths);
        } catch (Exception e) {
        }

        final List<String> basePathComponents = basePath.getComponents();
        final int baseDirsToTrim;
        if (commonParentDirsToRetain == null) {
            // no help from BioFormats

            // find the length of the shortest path, including file name
            int smallestPathLength;
            if (fullPaths.isEmpty())
                smallestPathLength = 1; /* imaginary file name */
            else {
                smallestPathLength = Integer.MAX_VALUE;
                for (final FsFile path : fullPaths) {
                    final int pathLength = path.getComponents().size();
                    if (smallestPathLength > pathLength)
                        smallestPathLength = pathLength;
                }
            }

            // plan to trim to try to retain a certain number of parent directories
            baseDirsToTrim = smallestPathLength - parentDirsToRetain - (1 /* file name */);
        } else
            // plan to trim the common root according to BioFormats' suggestion
            baseDirsToTrim = basePathComponents.size() - commonParentDirsToRetain;
        if (baseDirsToTrim < 0)
            return new Paths(basePath, fullPaths);
        // actually do the trimming
        basePath = new FsFile(basePathComponents.subList(baseDirsToTrim, basePathComponents.size()));
        final List<FsFile> trimmedPaths = new ArrayList<FsFile>(fullPaths.size());
        for (final FsFile path : fullPaths) {
            final List<String> pathComponents = path.getComponents();
            trimmedPaths.add(new FsFile(pathComponents.subList(baseDirsToTrim, pathComponents.size())));
        }
        return new Paths(basePath, trimmedPaths);
    }

    /**
     * Take a relative path that the user would like to see in his or her
     * upload area, and provide an import location instance whose paths
     * correspond to existing directories corresponding to the sanitized
     * file paths.
     * @param relPath Path parsed from the template
     * @param basePath Common base of all the listed paths ("/my/path")
     * @param reader BioFormats reader for data, may be null
     * @param checksumAlgorithm the checksum algorithm to use in verifying the integrity of uploaded files
     * @return {@link ImportLocation} instance
     */
    protected ImportLocation suggestImportPaths(FsFile relPath, FsFile basePath, List<FsFile> paths,
            Class<? extends FormatReader> reader, ChecksumAlgorithm checksumAlgorithm, Ice.Current __current)
            throws omero.ServerError {
        final Paths trimmedPaths = trimPaths(basePath, paths, reader);
        basePath = trimmedPaths.basePath;
        paths = trimmedPaths.fullPaths;

        // validate paths
        this.filePathNamingValidator.validateFilePathNaming(relPath);
        this.filePathNamingValidator.validateFilePathNaming(basePath);
        for (final FsFile path : paths) {
            this.filePathNamingValidator.validateFilePathNaming(path);
        }

        // Static elements which will be re-used throughout
        final ManagedImportLocationI data = new ManagedImportLocationI(); // Return value
        data.logFile = checkPath(relPath.toString() + ".log", checksumAlgorithm, __current);

        // try actually making directories
        final FsFile newBase = FsFile.concatenate(relPath, basePath);
        data.sharedPath = newBase.toString();
        data.usedFiles = new ArrayList<String>(paths.size());
        data.checkedPaths = new ArrayList<CheckedPath>(paths.size());
        for (final FsFile path : paths) {
            final String relativeToEnd = path.getPathFrom(basePath).toString();
            data.usedFiles.add(relativeToEnd);
            final String fullRepoPath = data.sharedPath + FsFile.separatorChar + relativeToEnd;
            data.checkedPaths.add(new CheckedPath(this.serverPaths, fullRepoPath, this.checksumProviderFactory,
                    checksumAlgorithm));
        }

        // Assuming we reach here, then we need to make
        // sure that the directory exists since the call
        // to saveFileset() requires the parent dirs to
        // exist.
        List<CheckedPath> dirs = new ArrayList<CheckedPath>();
        Set<String> seen = new HashSet<String>();
        dirs.add(checkPath(data.sharedPath, checksumAlgorithm, __current));
        for (CheckedPath checked : data.checkedPaths) {
            if (!seen.contains(checked.getRelativePath())) {
                dirs.add(checked.parent());
                seen.add(checked.getRelativePath());
            }
        }
        repositoryDao.makeDirs(this, dirs, true, __current);

        return data;
    }

    /**
     * @param x a collection of items, not {@code null}
     * @param y a collection of items, not {@code null}
     * @return if the collections have the same items in the same order, or if one is a prefix of the other
     */
    private static boolean isConsistentPrefixes(Iterable<?> x, Iterable<?> y) {
        final Iterator<?> xIterator = x.iterator();
        final Iterator<?> yIterator = y.iterator();
        while (xIterator.hasNext() && yIterator.hasNext()) {
            if (!xIterator.next().equals(yIterator.next())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Checks for the top-level user directory restriction before calling
     * {@link PublicRepositoryI#makeCheckedDirs(LinkedList<CheckedPath>, boolean, Current)}
     */
    @Override
    protected void makeCheckedDirs(final LinkedList<CheckedPath> paths, boolean parents, Session s,
            ServiceFactory sf, SqlAction sql, ome.system.EventContext effectiveEventContext) throws ServerError {

        final IAdmin adminService = sf.getAdminService();
        final EventContext ec = IceMapper.convert(effectiveEventContext);
        final String expanded = expandTemplate(template, ec);
        final FsFile rootOwnedPath = splitPath(expanded).getKey();
        final List<CheckedPath> pathsToFix = new ArrayList<CheckedPath>();
        final List<CheckedPath> pathsForRoot;

        /* if running as root then the paths must be root-owned */
        final long rootId = adminService.getSecurityRoles().getRootId();
        if (adminService.getEventContext().getCurrentUserId() == rootId) {
            pathsForRoot = ImmutableList.copyOf(paths);
        } else {
            pathsForRoot = ImmutableList.of();
        }

        for (int i = 0; i < paths.size(); i++) {

            CheckedPath checked = paths.get(i);
            if (checked.isRoot) {
                // This shouldn't happen but just in case.
                throw new ResourceError(null, null, "Cannot re-create root!");
            }

            /* check that the path is consistent with the root-owned template path directories */
            if (!isConsistentPrefixes(rootOwnedPath.getComponents(), checked.fsFile.getComponents())) {
                throw new omero.ValidationException(null, null, "cannot create directory \"" + checked.fsFile
                        + "\" with template path's root-owned \"" + rootOwnedPath + "\"");
            }

            pathsToFix.add(checked);
        }

        super.makeCheckedDirs(paths, parents, s, sf, sql, effectiveEventContext);

        /* ensure that root segment of the template path is wholly root-owned */
        if (!pathsForRoot.isEmpty()) {
            final Experimenter rootUser = sf.getQueryService().find(Experimenter.class, rootId);
            final IUpdate updateService = sf.getUpdateService();
            for (final CheckedPath pathForRoot : pathsForRoot) {
                final OriginalFile directory = repositoryDao.findRepoFile(sf, sql, getRepoUuid(), pathForRoot,
                        null);
                if (directory.getDetails().getOwner().getId() != rootId) {
                    directory.getDetails().setOwner(rootUser);
                    updateService.saveObject(directory);
                }
            }
        }

        // Now that we know that these are the right directories for
        // the current user, we make sure that the directories are in
        // the user group.
        repositoryDao.createOrFixUserDir(getRepoUuid(), pathsToFix, s, sf, sql);
    }
}