/* * 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.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.DateFormatSymbols; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import loci.formats.FormatReader; import ome.api.IAdmin; import ome.api.IUpdate; import ome.conditions.ApiUsageException; import ome.formats.importer.ImportConfig; import ome.formats.importer.ImportContainer; import ome.model.core.OriginalFile; import ome.model.meta.Experimenter; import ome.services.blitz.repo.path.ClientFilePathTransformer; import ome.services.blitz.repo.path.FilePathRestrictionInstance; import ome.services.blitz.repo.path.FsFile; import ome.services.blitz.repo.path.MakeNextDirectory; import ome.services.blitz.util.ChecksumAlgorithmMapper; import ome.system.Roles; import ome.system.ServiceFactory; import ome.util.SqlAction; import ome.util.checksum.ChecksumProvider; import ome.util.checksum.ChecksumProviderFactory; import ome.util.checksum.ChecksumProviderFactoryImpl; import ome.util.checksum.ChecksumType; 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.IndexingJobI; import omero.model.Job; import omero.model.MetadataImportJob; import omero.model.MetadataImportJobI; import omero.model.NamedValue; 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.StringUtils; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import Ice.Current; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.math.IntMath; /** * 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 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)); /* template paths: matches any special expansion term */ private static final Pattern TEMPLATE_TERM = Pattern.compile("%([a-zA-Z]+)(:([^%/]+))?%"); /* template paths: the root and user portions separately, never null */ /*private final*/ protected FsFile templateRoot; /* exposed for unit testing only */ private final FsFile templateUser; 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); int splitPoint = template.lastIndexOf("//"); if (splitPoint < 0) { /* without "//" the whole path is user-owned */ splitPoint = 0; } this.templateRoot = new FsFile(template.substring(0, splitPoint)); this.templateUser = new FsFile(template.substring(splitPoint)); if (FsFile.emptyPath.equals(templateUser)) { throw new omero.ApiUsageException(null, null, "no user-owned directories in managed repository template path"); } this.processes = processes; this.rootSessionUuid = rootSessionUuid; this.userGroupId = roles.getUserGroupId(); log.info("Repository template: " + template); } @Override public Ice.Object tie() { return new _ManagedRepositoryTie(this); } // // 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())); } // at this point, the template path should not yet exist on the filesystem final List<FsFile> sortedPaths = Ordering.usingToString().immutableSortedCopy(paths); final FsFile relPath = createTemplatePath(sortedPaths, __current); 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) { throw new IceMapper().handleServerError(e, context); } 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; } public List<Long> verifyChecksums(List<Long> ids, Current __current) throws ServerError { /* set up an invocation context in which the group is set to -1, for "all groups" */ final Current allGroupsCurrent = makeAdjustedCurrent(__current); allGroupsCurrent.ctx = new HashMap<String, String>(__current.ctx); allGroupsCurrent.ctx.put(omero.constants.GROUP.value, "-1"); /* verify the checksum of the specified files that are in this repository */ final List<Long> mismatchFiles = new ArrayList<Long>(); for (final long id : repositoryDao.filterFilesByRepository(getRepoUuid(), ids, allGroupsCurrent)) { /* get one of the files */ final OriginalFile file = repositoryDao.getOriginalFileWithHasher(id, allGroupsCurrent); final FsFile fsPath = new FsFile(file.getPath() + file.getName()); final String osPath = serverPaths.getServerFileFromFsFile(fsPath).getAbsolutePath(); /* check the file's checksum */ final ome.model.enums.ChecksumAlgorithm hasher = file.getHasher(); final String hash = file.getHash(); if (hasher != null && hash != null) { /* has a valid checksum, so check it */ final ChecksumProvider fromProvider = checksumProviderFactory.getProvider(ChecksumAlgorithmMapper.getChecksumType(hasher)); fromProvider.putFile(osPath); if (!fromProvider.checksumAsString().equalsIgnoreCase(hash)) { mismatchFiles.add(id); } } } return mismatchFiles; } public List<Long> setChecksumAlgorithm(ChecksumAlgorithm toHasherWrapped, List<Long> ids, Current __current) throws ServerError { /* set up an invocation context in which the group may be adjusted freely */ final Current adjustedGroupCurrent = makeAdjustedCurrent(__current); adjustedGroupCurrent.ctx = new HashMap<String, String>(__current.ctx); adjustedGroupCurrent.ctx.put(omero.constants.GROUP.value, "-1"); /* get the hasher to which to set the files */ final String toHasherName = toHasherWrapped.getValue().getValue(); final ome.model.enums.ChecksumAlgorithm toHasher = repositoryDao.getChecksumAlgorithm(toHasherName, adjustedGroupCurrent); final ChecksumType toType = ChecksumAlgorithmMapper.getChecksumType(toHasher); /* set the specified files that are in this repository */ final List<Long> adjustedFiles = new ArrayList<Long>(); for (final long id : repositoryDao.filterFilesByRepository(getRepoUuid(), ids, adjustedGroupCurrent)) { /* get one of the files */ final OriginalFile file = repositoryDao.getOriginalFileWithHasher(id, adjustedGroupCurrent); final FsFile fsPath = new FsFile(file.getPath() + file.getName()); final String osPath = serverPaths.getServerFileFromFsFile(fsPath).getAbsolutePath(); /* check the file's existing hasher */ final ome.model.enums.ChecksumAlgorithm fromHasher = file.getHasher(); final String fromHash = file.getHash(); ChecksumProvider fromProvider = null; if (fromHasher != null && fromHash != null) { /* already has a valid hash */ if (toHasherName.equals(fromHasher.getValue())) { /* already hashed in the specified manner */ continue; } else { /* hashed with a different hasher */ fromProvider = checksumProviderFactory.getProvider(ChecksumAlgorithmMapper.getChecksumType(fromHasher)); } } /* find the new hash */ final ChecksumProvider toProvider = checksumProviderFactory.getProvider(toType); toProvider.putFile(osPath); final String toHash = toProvider.checksumAsString(); if (fromProvider != null) { /* check old hash after new one is calculated */ fromProvider.putFile(osPath); if (!fromProvider.checksumAsString().equals(fromHash)) { throw new ServerError(null, null, "hash mismatch on file ID " + id); } } /* update the file's checksum */ file.setHasher(toHasher); file.setHash(toHash); final String fileGroup = Long.toString(file.getDetails().getGroup().getId()); adjustedGroupCurrent.ctx.put(omero.constants.GROUP.value, fileGroup); repositoryDao.saveObject(file, adjustedGroupCurrent); adjustedGroupCurrent.ctx.put(omero.constants.GROUP.value, "-1"); adjustedFiles.add(id); } return adjustedFiles; } // // 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 List<NamedValue> serverVersionInfo = new ArrayList<NamedValue>(); 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) { final List<NamedValue> versionInfo = ((UploadJob) job).getVersionInfo(__current); if (versionInfo == null) { continue; } String readerName = null; for (NamedValue nv : versionInfo) { if (nv != null && ImportConfig.VersionInfo.BIO_FORMATS_READER.key.equals( nv.name)) { readerName = nv.value; break; } } if (readerName == null) { continue; } Class<?> potentialReaderClass; try { 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 paths 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); } /** * Manages the expansion of template paths. Expected to be superseded by a more general approach. * @author m.t.b.carroll@dundee.ac.uk * @since 5.0.3 */ private class TemplateDirectoryCreator { private final Calendar now = Calendar.getInstance(); private final EventContext ctx; private final Object consistentData; private final boolean createDirectories; private final Ice.Current current; private final ServiceFactory sf; private final Deque<String> remaining; private final List<String> done; /** * Prepare to expand a template path. * @param base the pre-existing parent directories in the repository * @param todo the template path to expand * @param ctx the context to apply in expanding the template path * @param consistentData the data from which to calculate a consistent hash * @param createDirectories if this instance should create the template path on the file-system * @param current the method invocation context in which to perform queries and create directories * {@code null} to omit actual directory creation */ TemplateDirectoryCreator(FsFile base, FsFile todo, final EventContext ctx, final Object consistentData, boolean createDirectories, Current current) { this.ctx = ctx; this.consistentData = consistentData; this.createDirectories = createDirectories; this.current = current; this.sf = null; this.remaining = new ArrayDeque<String>(todo.getComponents()); this.done = new ArrayList<String>(base.getComponents()); } /** * Prepare to expand a template path. * @param base the pre-existing parent directories in the repository * @param todo the template path to expand * @param ctx the context to apply in expanding the template path * @param consistentData the data from which to calculate a consistent hash * @param createDirectories if this instance should create the template path on the file-system * (must be {@code false} for this constructor) * @param sf the service factory which to perform queries * {@code null} to omit actual directory creation */ TemplateDirectoryCreator(FsFile base, FsFile todo, final EventContext ctx, final Object consistentData, boolean createDirectories, ServiceFactory sf) { if (createDirectories) { throw new ApiUsageException("may not create directories with only a service factory"); } this.ctx = ctx; this.consistentData = consistentData; this.createDirectories = createDirectories; this.current = null; this.sf = sf; this.remaining = new ArrayDeque<String>(todo.getComponents()); this.done = new ArrayList<String>(base.getComponents()); } /** * Concatenate the given path on to the end of the directories already expanded, and return that full repository path. * @param path a list of subdirectories * @return the full path */ private String getFullPathWith(List<String> path) { final StringBuffer sb = new StringBuffer(); for (final String component : Iterables.concat(done, path)) { sb.append(FsFile.separatorChar); sb.append(component); } return sb.toString(); } /** * Expand {@code %user%} to the user's name. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandUser(String prefix, String suffix) { return prefix + ctx.userName + suffix; } /** * Expand {@code %userid%} to the user's ID. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandUserId(String prefix, String suffix) { return prefix + ctx.userId + suffix; } /** * Expand {@code %group%} to the group's name. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandGroup(String prefix, String suffix) { return prefix + ctx.groupName + suffix; } /** * Expand {@code %groupid%} to the group's ID. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandGroupId(String prefix, String suffix) { return prefix + ctx.groupId + suffix; } /** * Expand {@code %year%} to the current year. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandYear(String prefix, String suffix) { return prefix + now.get(Calendar.YEAR) + suffix; } /** * Expand {@code %month%} to the current month number. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandMonth(String prefix, String suffix) { return prefix + String.format("%02d", now.get(Calendar.MONTH) + 1) + suffix; } /** * Expand {@code %monthname%} to the current month name. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandMonthname(String prefix, String suffix) { return prefix + DATE_FORMAT.getMonths()[now.get(Calendar.MONTH)] + suffix; } /** * Expand {@code %day%} to the current day number in the month. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandDay(String prefix, String suffix) { return prefix + String.format("%02d", now.get(Calendar.DAY_OF_MONTH)) + suffix; } /** * Expand {@code %time%} to the current hour, minute, second and millisecond. * The directory is actually created. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @throws ServerError if the directory could not be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public void expandAndCreateTime(final String prefix, final String suffix) throws ServerError { final MakeNextDirectory directoryMaker = new MakeNextDirectory() { @Override public List<String> getPathFor(long index) { final int hour = now.get(Calendar.HOUR_OF_DAY); final int minute = now.get(Calendar.MINUTE); final int second = now.get(Calendar.SECOND); final long millisecond = now.get(Calendar.MILLISECOND) + index; final String time = String.format("%s%02d-%02d-%02d.%03d%s", prefix, hour, minute, second, millisecond, suffix); return Collections.singletonList(time); } @Override public boolean isAcceptable(List<String> path) throws ServerError { return !checkPath(getFullPathWith(path), null, current).exists(); } @Override public void usePath(List<String> path) throws ServerError { makeDir(getFullPathWith(path), false, current); } }; done.addAll(directoryMaker.useFirstAcceptable()); } /** * Expand {@code %session%} to the session's UUID. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandSession(String prefix, String suffix) { return prefix + ctx.sessionUuid + suffix; } /** * Expand {@code %sessionid%} to the session's ID. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandSessionId(String prefix, String suffix) { return prefix + ctx.sessionId + suffix; } /** * Expand {@code %perms%} to the group's permissions. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandPerms(String prefix, String suffix) { return prefix + ctx.groupPermissions + suffix; } /** * Expand {@code %institution%} to the user's institution, omitting this component if they do not have one. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandInstitution(String prefix, String suffix) { final String institution; if (current != null) { institution = repositoryDao.getUserInstitution(ctx.userId, current); } else { institution = repositoryDao.getUserInstitution(ctx.userId, sf); } if (StringUtils.isBlank(institution)) { return null; } else { return prefix + serverPaths.getPathSanitizer().apply(institution) + suffix; } } /** * Expand {@code %institution%} to the user's institution, using a default if they do not have one. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @param defaultForNone the string to use as the institution of users who do not have one set for them * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandInstitution(String prefix, String suffix, String defaultForNone) { String institution; if (current != null) { institution = repositoryDao.getUserInstitution(ctx.userId, current); } else { institution = repositoryDao.getUserInstitution(ctx.userId, sf); } if (StringUtils.isBlank(institution)) { institution = defaultForNone; } return prefix + serverPaths.getPathSanitizer().apply(institution) + suffix; } /** * Expand {@code %hash%} to a consistent hash of eight hexadecimal digits. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created * @throws ServerError if the expansion term was improperly specified */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandHash(String prefix, String suffix) throws ServerError { return expandHash(prefix, suffix, "8"); } /** * Expand {@code %hash%} to a consistent hash of the given number of hexadecimal digits. * Further comma-separated digits use more of the hash in subdirectories. * @param prefix path component text preceding the expansion term in the first directory, may be empty * @param suffix path component text following the expansion term in the first directory, may be empty * @param parameters a comma-separated list of how many hexadecimal digits of the hash to use in each directory * @return entire replaced path component, may be unchanged to be revisited, * or {@code null} if it has been wholly processed; otherwise it will be created * @throws ServerError if the expansion term was improperly specified */ // @SuppressWarnings("unused") /* used by create() via Method.invoke */ public String expandHash(String prefix, String suffix, String parameters) throws ServerError { if (consistentData == null) { throw new ServerError(null, null, "%hash% is prohibited in this part of the repository template path"); } /* simple zero-padding regardless of the hash code's sign */ final String hash = Long.toHexString(0x200000000l + consistentData.hashCode()).substring(1).toUpperCase(); final Deque<String> components = new ArrayDeque<String>(); int currentPosition = 0; for (final String digitCount : Splitter.on(',').split(parameters)) { final int length = Integer.parseInt(digitCount); if (length < 1 || length + currentPosition > hash.length()) { throw new ServerError(null, null, "invalid parameters \"" + parameters + "\" for %hash% in the repository template path"); } components.push(prefix + hash.substring(currentPosition, currentPosition + length) + suffix); currentPosition += length; /* apply prefix and suffix to first directory only */ prefix = ""; suffix = ""; } while (!components.isEmpty()) { remaining.push(components.pop()); } return null; } /** * Expand {@code %increment%} to a uniquely named directory, counting by natural numbers. * The directory is actually created. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @throws ServerError if the directory could not be created or the expansion term was improperly specified */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public void expandAndCreateIncrement(String prefix, String suffix) throws ServerError { expandAndCreateIncrement(prefix, suffix, "0"); } /** * Expand {@code %increment%} to a uniquely named directory, counting by natural numbers. * The directory is actually created. * @param prefix path component text preceding the expansion term, may be empty * @param suffix path component text following the expansion term, may be empty * @param paddingString the minimum number of digits for the natural number, achieved by zero-padding if necessary * @throws ServerError if the directory could not be created or the expansion term was improperly specified */ // @SuppressWarnings("unused") /* used by create() via Method.invoke */ public void expandAndCreateIncrement(final String prefix, final String suffix, String paddingString) throws ServerError { final int padding = Integer.parseInt(paddingString); final MakeNextDirectory directoryMaker = new MakeNextDirectory() { @Override public List<String> getPathFor(long index) { return Collections.singletonList(prefix + Strings.padStart(Long.toString(index + 1), padding, '0') + suffix); } @Override public boolean isAcceptable(List<String> path) throws ServerError { return !checkPath(getFullPathWith(path), null, current).exists(); } @Override public void usePath(List<String> path) throws ServerError { makeDir(getFullPathWith(path), false, current); } }; done.addAll(directoryMaker.useFirstAcceptable()); } /** * Get the extra directories that correspond to the given natural number. * @param prefix path component text preceding the expansion term in the first directory, may be empty * @param suffix path component text following the expansion term in the first directory, may be empty * @param digits the power of ten that is the directory entry limit, e.g., {@code "3"} for one thousand * @param count the natural number identifying the set of extra directories * @return the extra directories */ private List<String> getExtraSubdirectories(String prefix, String suffix, int digits, long count) { final List<String> subdirectories = new ArrayList<String>(); StringBuffer paddedCount = new StringBuffer(); paddedCount.append(count); /* make padded.length() a multiple of digits by zero padding */ while (paddedCount.length() % digits != 0) { paddedCount.insert(0, '0'); } /* and work through the digits-length groups */ for (int c = 0, l = paddedCount.length(); c < l; c += digits) { subdirectories.add(prefix + paddedCount.substring(c, c + digits) + suffix); /* apply prefix and suffix to first directory only */ prefix = ""; suffix = ""; } return subdirectories; } /** * Count the entries in the given directory. * @param path the repository path for the directory * @return the number of entries in the directory, or {@code 0} if the path does not exist but its parent is a directory, * or a very large number if the path cannot be created as a directory */ private int directoryContentsCount(String path) { final File directory = serverPaths.getServerFileFromFsFile(new FsFile(path)); if (directory.exists()) { if (directory.isDirectory()) { return directory.list().length; } } else { final File parent = directory.getParentFile(); if (parent != null && parent.exists() && parent.isDirectory()) { return 0; } } return Integer.MAX_VALUE; } /** * Expand {@code %subdirs%} to none or more directories such that the final one contains no more than one thousand entries. * These extra directories are added at the point in the path where the component mentioning {@code %subdirs%} occurs, when * the preceding directory has become sufficiently full. The directories are actually created. * @param prefix path component text preceding the expansion term in the first directory, may be empty * @param suffix path component text following the expansion term in the first directory, may be empty * @throws ServerError if the directory could not be created or the expansion term was improperly specified */ @SuppressWarnings("unused") /* used by create() via Method.invoke */ public void expandAndCreateSubdirs(String prefix, String suffix) throws ServerError { expandAndCreateSubdirs(prefix, suffix, "3"); } /** * Expand {@code %subdirs%} to none or more directories such that the final one contains no more than a certain number of * entries. These extra directories are added at the point in the path where the component mentioning {@code %subdirs%} * occurs, when the preceding directory has become sufficiently full. The directories are actually created. * @param prefix path component text preceding the expansion term in the first directory, may be empty * @param suffix path component text following the expansion term in the first directory, may be empty * @param digitsString the power of ten that is the directory entry limit, e.g., {@code "3"} for one thousand * @throws ServerError if the directory could not be created or the expansion term was improperly specified */ // @SuppressWarnings("unused") /* used by create() via Method.invoke */ public void expandAndCreateSubdirs(final String prefix, final String suffix, String digitsString) throws ServerError { final int digits = Integer.parseInt(digitsString); if (digits < 1) { throw new ServerError(null, null, "invalid parameter \"" + digitsString + "\" for %subdirs% in the repository template path"); } final int limit = IntMath.checkedPow(10, digits); if (directoryContentsCount(getFullPathWith(Collections.<String>emptyList())) < limit) { /* do not yet need to break out into subdirectories */ return; } final MakeNextDirectory directoryMaker = new MakeNextDirectory() { @Override public List<String> getPathFor(long index) { return getExtraSubdirectories(prefix, suffix, digits, index); } @Override public boolean isAcceptable(List<String> path) throws ServerError { return directoryContentsCount(getFullPathWith(path)) < limit; } @Override public void usePath(List<String> path) throws ServerError { makeDir(getFullPathWith(path), true, current); } }; done.addAll(directoryMaker.useFirstAcceptable()); } /** * Expand and create the template path. * @return the path * @throws ServerError if the path could not be expanded and created */ FsFile create() throws ServerError { while (!remaining.isEmpty()) { /* work on next directory component */ String pattern = remaining.pop(); Matcher matcher = TEMPLATE_TERM.matcher(pattern); boolean isMatcherPristine = true; while (pattern != null) { if (matcher.find()) { isMatcherPristine = false; } else { /* no terms still to review in this component */ if (isMatcherPristine) { /* and none to revisit, this component is done */ done.add(pattern); break; } else { /* no expansions occurred, else the matcher would have been reset */ throw new ServerError(null, null, "cannot expand template repository path component \"" + pattern + '"'); } } /* examine the term to expand */ final String prefix = pattern.substring(0, matcher.start()); final String suffix = pattern.substring(matcher.end()); final String term = matcher.group(1); String parameters = matcher.group(3); Method expander; /* try to expand the term */ final String oldPattern = pattern; final boolean isTryCreateDirectory = createDirectories && !(TEMPLATE_TERM.matcher(prefix).matches() || TEMPLATE_TERM.matcher(suffix).matches()); while (true) { try { if (parameters == null) { /* without parameters */ try { /* expand term only */ final String methodName = "expand" + StringUtils.capitalize(term); expander = getClass().getMethod(methodName, String.class, String.class); pattern = (String) expander.invoke(this, prefix, suffix); } catch (NoSuchMethodException e1) { if (isTryCreateDirectory) { try { /* expand term and create directory */ final String methodName = "expandAndCreate" + StringUtils.capitalize(term); expander = getClass().getMethod(methodName, String.class, String.class); expander.invoke(this, prefix, suffix); pattern = null; } catch (NoSuchMethodException e2) { /* move on */ } } } /* tried without parameters, so move on */ break; } else { /* with parameters */ try { /* expand term only */ final String methodName = "expand" + StringUtils.capitalize(term); expander = getClass().getMethod(methodName, String.class, String.class, String.class); pattern = (String) expander.invoke(this, prefix, suffix, parameters); break; } catch (NoSuchMethodException e1) { if (isTryCreateDirectory) { try { /* expand term and create directory */ final String methodName = "expandAndCreate" + StringUtils.capitalize(term); expander = getClass().getMethod(methodName, String.class, String.class, String.class); expander.invoke(this, prefix, suffix, parameters); pattern = null; break; } catch (NoSuchMethodException e2) { /* try without parameters */ } } } /* failed with parameters, so try without */ log.warn("ignoring parameters \"" + parameters + "\" on \"" + matcher.group(0) + "\" in repository template path"); parameters = null; } } catch (InvocationTargetException e) { /* try to unwrap underlying exception from expansion method invocation */ final Throwable cause = e.getCause(); if (cause instanceof ServerError) { throw (ServerError) cause; } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else { final String message = "unexpected exception in expanding \"" + pattern + '"'; throw new ServerError(null, null, message, cause); } } catch (ReflectiveOperationException e) { final String message = "unexpected exception in expanding \"" + pattern + '"'; throw new ServerError(null, null, message, e); } } if (!(pattern == null || oldPattern.equals(pattern))) { /* successful expansion, so match against the new form of this component */ matcher = TEMPLATE_TERM.matcher(pattern); isMatcherPristine = true; } } if (pattern != null && createDirectories) { /* expansion occurred but directory was not yet created */ makeDir(new FsFile(done).toString(), !remaining.isEmpty(), current); } } /* all components now processed */ return new FsFile(done); } } /** * Expand the root-owned segment of the template path. * @param ctx the event context to apply in expanding terms * @param current the method invocation context in which to perform queries * @return the expanded template path * @throws ServerError if the path could not be expanded */ private FsFile expandTemplateRootOwnedPath(EventContext ctx, Current current) throws ServerError { return new TemplateDirectoryCreator(FsFile.emptyPath, templateRoot, ctx, null, false, current).create(); } /** * Expand the root-owned segment of the template path. * @param ctx the event context to apply in expanding terms * @param sf the service factory which to perform queries * @return the expanded template path * @throws ServerError if the path could not be expanded */ /* exposed for unit testing only */ /*private*/ protected FsFile expandTemplateRootOwnedPath(EventContext ctx, ServiceFactory sf) throws ServerError { return new TemplateDirectoryCreator(FsFile.emptyPath, templateRoot, ctx, null, false, sf).create(); } /** * Expand and create the user-owned segment of the template path. * @param ctx the event context to apply in expanding terms * @param rootBase the expanded root-owned segment of the template path * @param Object consistentData the object to hash in expanding {@code %hash%} * @param current the method invocation context in which to perform queries and create directories * @return the expanded template path * @throws ServerError if the path could not be expanded and created */ private FsFile expandAndCreateTemplateUserOwnedPath(EventContext ctx, FsFile rootBase, Object consistentData, Current current) throws ServerError { return new TemplateDirectoryCreator(rootBase, templateUser, ctx, consistentData, true, current).create(); } /** * Expand the template path and create its directories with the correct ownership. * @param consistentData the object to hash in expanding {@code %hash%} * @param __current the current ICE method invocation context * @return the expanded template path * @throws ServerError if the new path could not be created */ protected FsFile createTemplatePath(Object consistentData, Ice.Current __current) throws ServerError { final EventContext ctx = repositoryDao.getEventContext(__current); final FsFile rootOwnedExpanded; if (FsFile.emptyPath.equals(templateRoot)) { rootOwnedExpanded = FsFile.emptyPath; } else { /* there are some root-owned directories first */ rootOwnedExpanded = expandTemplateRootOwnedPath(ctx, __current); final Current rootCurr = sudo(__current, rootSessionUuid); rootCurr.ctx.put(omero.constants.GROUP.value, Long.toString(userGroupId)); makeDir(rootOwnedExpanded.toString(), true, rootCurr); } /* now create the user-owned directories */ final FsFile wholeExpanded = expandAndCreateTemplateUserOwnedPath(ctx, rootOwnedExpanded, consistentData, __current); if (wholeExpanded.equals(rootOwnedExpanded)) { throw new omero.ApiUsageException(null, null, "no user-owned directories in expanded form of managed repository template path"); } return wholeExpanded; } /** 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; // sanitize paths (should already be sanitary; could introduce conflicts) final Function<String, String> sanitizer = serverPaths.getPathSanitizer(); relPath = relPath.transform(sanitizer); basePath = basePath.transform(sanitizer); int index = paths.size(); while (--index >= 0) { paths.set(index, paths.get(index).transform(sanitizer)); } // 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, boolean, Session, ServiceFactory, SqlAction, ome.system.EventContext)} */ @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 FsFile rootOwnedPath = expandTemplateRootOwnedPath(ec, sf); 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); } }