org.artifactory.repo.service.RepositoryServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.repo.service.RepositoryServiceImpl.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.repo.service;

import com.google.common.collect.*;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.artifactory.addon.AddonsManager;
import org.artifactory.addon.NuGetAddon;
import org.artifactory.addon.WebstartAddon;
import org.artifactory.addon.gems.GemsAddon;
import org.artifactory.addon.replication.ReplicationAddon;
import org.artifactory.api.common.BasicStatusHolder;
import org.artifactory.api.common.MoveMultiStatusHolder;
import org.artifactory.api.config.CentralConfigService;
import org.artifactory.api.config.ExportSettingsImpl;
import org.artifactory.api.config.ImportSettingsImpl;
import org.artifactory.api.config.RepositoryImportSettingsImpl;
import org.artifactory.api.context.ContextHelper;
import org.artifactory.api.jackson.JacksonReader;
import org.artifactory.api.maven.MavenMetadataService;
import org.artifactory.api.module.ModuleInfo;
import org.artifactory.api.module.ModuleInfoUtils;
import org.artifactory.api.module.VersionUnit;
import org.artifactory.api.repo.ArchiveFileContent;
import org.artifactory.api.repo.Async;
import org.artifactory.api.repo.exception.FileExpectedException;
import org.artifactory.api.repo.exception.FolderExpectedException;
import org.artifactory.api.repo.exception.ItemNotFoundRuntimeException;
import org.artifactory.api.repo.exception.RepoRejectException;
import org.artifactory.api.request.UploadService;
import org.artifactory.api.rest.constant.RepositoriesRestConstants;
import org.artifactory.api.rest.constant.RestConstants;
import org.artifactory.api.search.ItemSearchResults;
import org.artifactory.api.search.SavedSearchResults;
import org.artifactory.api.search.SearchService;
import org.artifactory.api.search.VersionSearchResults;
import org.artifactory.api.search.deployable.VersionUnitSearchControls;
import org.artifactory.api.search.property.PropertySearchControls;
import org.artifactory.api.search.property.PropertySearchResult;
import org.artifactory.api.security.AclService;
import org.artifactory.api.security.AuthorizationService;
import org.artifactory.api.storage.StorageQuotaInfo;
import org.artifactory.binstore.BinaryInfo;
import org.artifactory.checksum.ChecksumInfo;
import org.artifactory.checksum.ChecksumType;
import org.artifactory.checksum.ChecksumsInfo;
import org.artifactory.common.ConstantValues;
import org.artifactory.common.MutableStatusHolder;
import org.artifactory.common.StatusHolder;
import org.artifactory.config.InternalCentralConfigService;
import org.artifactory.descriptor.config.CentralConfigDescriptor;
import org.artifactory.descriptor.repo.*;
import org.artifactory.exception.CancelException;
import org.artifactory.factory.InfoFactoryHolder;
import org.artifactory.fs.*;
import org.artifactory.info.InfoWriter;
import org.artifactory.io.StringResourceStreamHandle;
import org.artifactory.mbean.MBeanRegistrationService;
import org.artifactory.md.Properties;
import org.artifactory.mime.NamingUtils;
import org.artifactory.model.xstream.fs.PropertiesImpl;
import org.artifactory.repo.*;
import org.artifactory.repo.cleanup.FolderPruningService;
import org.artifactory.repo.count.ArtifactCountRetriever;
import org.artifactory.repo.db.DbLocalRepo;
import org.artifactory.repo.db.importexport.DbRepoExportSearchHandler;
import org.artifactory.repo.http.IdleConnectionMonitorService;
import org.artifactory.repo.interceptor.StorageInterceptors;
import org.artifactory.repo.local.PathDeletionContext;
import org.artifactory.repo.local.ValidDeployPathContext;
import org.artifactory.repo.mbean.ManagedRepository;
import org.artifactory.repo.service.mover.*;
import org.artifactory.repo.virtual.VirtualRepo;
import org.artifactory.request.InternalArtifactoryResponse;
import org.artifactory.request.InternalRequestContext;
import org.artifactory.request.NullRequestContext;
import org.artifactory.request.RepoRequests;
import org.artifactory.resource.FileResource;
import org.artifactory.resource.ResolvedResource;
import org.artifactory.resource.ResourceStreamHandle;
import org.artifactory.resource.UnfoundRepoResource;
import org.artifactory.sapi.common.BaseSettings;
import org.artifactory.sapi.common.ExportSettings;
import org.artifactory.sapi.common.ImportSettings;
import org.artifactory.sapi.common.RepositoryRuntimeException;
import org.artifactory.sapi.fs.*;
import org.artifactory.schedule.*;
import org.artifactory.search.InternalSearchService;
import org.artifactory.security.*;
import org.artifactory.spring.InternalContextHelper;
import org.artifactory.spring.Reloadable;
import org.artifactory.storage.StorageService;
import org.artifactory.storage.binstore.service.BinaryStore;
import org.artifactory.storage.fs.lock.LockingHelper;
import org.artifactory.storage.fs.service.FileService;
import org.artifactory.storage.fs.service.ItemMetaInfo;
import org.artifactory.storage.fs.service.NodeMetaInfoService;
import org.artifactory.storage.fs.service.PropertiesService;
import org.artifactory.storage.fs.tree.ItemNode;
import org.artifactory.storage.fs.tree.ItemTree;
import org.artifactory.storage.fs.tree.TreeBrowsingCriteria;
import org.artifactory.storage.fs.tree.TreeBrowsingCriteriaBuilder;
import org.artifactory.storage.jobs.StatsDelegatingServiceFlushJob;
import org.artifactory.storage.jobs.StatsPersistingServiceFlushJob;
import org.artifactory.storage.service.StatsServiceImpl;
import org.artifactory.util.*;
import org.artifactory.version.CompoundVersionDetails;
import org.codehaus.jackson.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Service
@Reloadable(beanClass = InternalRepositoryService.class, initAfter = { StorageInterceptors.class,
        InternalCentralConfigService.class, TaskService.class })
public class RepositoryServiceImpl implements InternalRepositoryService, LockableUndeploy {
    private static final Logger log = LoggerFactory.getLogger(RepositoryServiceImpl.class);

    private static final String REPOSITORIES_MBEAN_TYPE = "Repositories";

    @Autowired
    private AclService aclService;

    @Autowired
    private SearchService artifactsearchService;

    @Autowired
    private AuthorizationService authService;

    @Autowired
    private CentralConfigService centralConfigService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private MavenMetadataService mavenMetadataService;

    @Autowired
    private InternalSearchService searchService;

    @Autowired
    private AddonsManager addonsManager;

    @Autowired
    private UploadService uploadService;

    @Autowired
    private StorageService storageService;

    @Autowired
    private StatsServiceImpl statsService;

    @Autowired
    private FileService fileService;

    @Autowired
    private BinaryStore binaryStore;

    @Autowired
    private FolderPruningService pruneService;

    private ArtifactCountRetriever artifactCountRetriever;

    private VirtualRepo globalVirtualRepo;

    private Map<String, VirtualRepo> virtualRepositoriesMap = Maps.newLinkedHashMap();

    // a cache of all the repository keys
    private Set<String> allRepoKeysCache;

    private static InternalRepositoryService getTransactionalMe() {
        return InternalContextHelper.get().beanForType(InternalRepositoryService.class);
    }

    @Override
    public void init() {
        rebuildRepositories(null);
        HttpUtils.resetArtifactoryUserAgent();

        try {
            //Dump info to the log
            InfoWriter.writeInfo();
        } catch (Exception e) {
            log.warn("Failed dumping system info", e);
        }

        // register internal statistics flushing job
        TaskBase localStatsFlushTask = TaskUtils.createRepeatingTask(StatsPersistingServiceFlushJob.class,
                TimeUnit.SECONDS.toMillis(ConstantValues.statsFlushIntervalSecs.getLong()),
                TimeUnit.SECONDS.toMillis(ConstantValues.statsFlushIntervalSecs.getLong()));

        // register remote statistics flushing job
        TaskBase remoteFlushTask = TaskUtils.createRepeatingTask(StatsDelegatingServiceFlushJob.class,
                TimeUnit.SECONDS.toMillis(ConstantValues.statsRemoteFlushIntervalSecs.getLong()),
                TimeUnit.SECONDS.toMillis(ConstantValues.statsRemoteFlushIntervalSecs.getLong()));

        taskService.startTask(localStatsFlushTask, false);
        taskService.startTask(remoteFlushTask, false);
    }

    @Override
    public void reload(CentralConfigDescriptor oldDescriptor) {
        HttpUtils.resetArtifactoryUserAgent();
        deleteOrphanRepos(oldDescriptor);
        rebuildRepositories(oldDescriptor);
        checkAndCleanChangedVirtualPomCleanupPolicy(oldDescriptor);
    }

    @Override
    @Async(authenticateAsSystem = true)
    public void onContextReady() {
        //
        registerRepositoriesMBeans();

        //
        GemsAddon gemsAddon = addonsManager.addonByType(GemsAddon.class);
        if (!gemsAddon.isDefault()) {
            for (LocalRepo localRepo : globalVirtualRepo.getLocalRepositories()) {
                if (!localRepo.isBlackedOut()) {
                    if (localRepo.getDescriptor().getType().equals(RepoType.Gems)) {
                        gemsAddon.afterRepoInit(localRepo.getKey());
                    }
                }
            }
            for (VirtualRepo virtualRepo : getVirtualRepositories()) {
                if (virtualRepo.getDescriptor().getType().equals(RepoType.Gems)) {
                    gemsAddon.afterRepoInit(virtualRepo.getKey());
                }
            }
        }

        //
        NuGetAddon nuGetAddon = addonsManager.addonByType(NuGetAddon.class);
        if (!nuGetAddon.isDefault()) {
            for (LocalRepo localRepo : globalVirtualRepo.getLocalRepositories()) {
                if (!localRepo.isBlackedOut()) {
                    if (localRepo.getDescriptor().getType().equals(RepoType.NuGet)) { //feature
                        nuGetAddon.afterRepoInit(localRepo.getKey());
                    }
                }
            }
        }
    }

    @Override
    public void onContextCreated() {
    }

    @Override
    public void onContextUnready() {
    }

    private void checkAndCleanChangedVirtualPomCleanupPolicy(CentralConfigDescriptor oldDescriptor) {
        Map<String, VirtualRepoDescriptor> oldVirtualDescriptors = oldDescriptor.getVirtualRepositoriesMap();
        List<VirtualRepoDescriptor> newVirtualDescriptors = getVirtualRepoDescriptors();
        for (VirtualRepoDescriptor newDescriptor : newVirtualDescriptors) {
            String repoKey = newDescriptor.getKey();
            VirtualRepoDescriptor oldVirtualDescriptor = oldVirtualDescriptors.get(repoKey);
            if (oldVirtualDescriptor != null && pomCleanUpPolicyChanged(newDescriptor, oldVirtualDescriptor)) {
                VirtualRepo virtualRepo = virtualRepositoryByKey(repoKey);
                log.info("Pom Repository Reference Cleanup Policy changed in '{}', cleaning repository cache. ",
                        repoKey);
                RepoPath rootPath = InternalRepoPathFactory.repoRootPath(repoKey);
                virtualRepo.undeploy(rootPath, false);
            }
        }
    }

    private boolean pomCleanUpPolicyChanged(VirtualRepoDescriptor newDescriptor,
            VirtualRepoDescriptor oldDescriptor) {
        PomCleanupPolicy newPolicy = newDescriptor.getPomRepositoryReferencesCleanupPolicy();
        PomCleanupPolicy oldPolicy = oldDescriptor.getPomRepositoryReferencesCleanupPolicy();
        return !newPolicy.equals(oldPolicy);
    }

    private void deleteOrphanRepos(CentralConfigDescriptor oldDescriptor) {
        CentralConfigDescriptor currentDescriptor = centralConfigService.getDescriptor();
        Set<String> newRepoKeys = getConfigRepoKeys(currentDescriptor);
        Set<String> oldRepoKeys = getConfigRepoKeys(oldDescriptor);
        for (String key : oldRepoKeys) {
            if (!newRepoKeys.contains(key)) {
                log.warn("Removing the no-longer-referenced repository " + key);
                StatusHolder statusHolder = deleteOrphanRepo(key);
                if (statusHolder.isError()) {
                    log.warn("Error occurred during repo '{}' removal: {}", key, statusHolder.getStatusMsg());
                }
            }
        }
    }

    //TORE: [by YS] delete from the db directly - there's no need for permissions checks, events etc.
    private StatusHolder deleteOrphanRepo(String repoKey) {
        BasicStatusHolder status = new BasicStatusHolder();
        StoringRepo storingRepo = storingRepositoryByKey(repoKey);
        if (storingRepo == null) {
            status.warn("Repo not found for deletion: " + repoKey, log);
            return status;
        }

        //Delete all acl references to the repository being deleted
        List<AclInfo> acls = aclService.getAllAcls();
        for (AclInfo aclInfo : acls) {
            MutablePermissionTargetInfo permissionTarget = InfoFactoryHolder.get()
                    .copyPermissionTarget(aclInfo.getPermissionTarget());
            String cachedRepoKey = repoKey.concat(LocalCacheRepoDescriptor.PATH_SUFFIX); //for remote repos
            List<String> repoKeys = permissionTarget.getRepoKeys();
            if (repoKeys.remove(repoKey) || repoKeys.remove(cachedRepoKey)) {
                MutableAclInfo mutableAclInfo = InfoFactoryHolder.get().copyAcl(aclInfo);
                permissionTarget.setRepoKeys(repoKeys);
                mutableAclInfo.setPermissionTarget(permissionTarget);
                aclService.updateAcl(mutableAclInfo);
            }
        }

        MutableVfsFolder rootFolder = storingRepo.getMutableFolder(storingRepo.getRepoPath(""));
        if (rootFolder == null) {
            status.warn("Root folder not found for deletion: " + repoKey, log);
            return status;
        }

        rootFolder.deleteIncludingRoot();
        return status;
    }

    private Set<String> getConfigRepoKeys(CentralConfigDescriptor descriptor) {
        Set<String> repoKeys = new HashSet<>();
        repoKeys.addAll(descriptor.getLocalRepositoriesMap().keySet());
        repoKeys.addAll(descriptor.getRemoteRepositoriesMap().keySet());
        repoKeys.addAll(descriptor.getVirtualRepositoriesMap().keySet());
        return repoKeys;
    }

    @Override
    public void destroy() {
        List<Repo> repos = Lists.newArrayList();
        repos.addAll(getVirtualRepositories());
        repos.addAll(getLocalAndRemoteRepositories());
        for (Repo repo : repos) {
            try {
                repo.destroy();
            } catch (Exception e) {
                log.error("Error while destroying the repository '{}'.", repo, e);
            }
        }
    }

    @Override
    public void convert(CompoundVersionDetails source, CompoundVersionDetails target) {
    }

    private void rebuildRepositories(CentralConfigDescriptor oldDescriptor) {
        if (globalVirtualRepo != null) {
            // stop remote repo online monitors
            for (RemoteRepo remoteRepo : globalVirtualRepo.getRemoteRepositories()) {
                remoteRepo.cleanupResources();
            }
        }

        //Create the repository objects from the descriptor
        CentralConfigDescriptor centralConfig = centralConfigService.getDescriptor();
        InternalRepositoryService transactionalMe = getTransactionalMe();

        //Local repos
        Map<String, LocalRepo> localRepositoriesMap = Maps.newLinkedHashMap();
        Map<String, LocalRepoDescriptor> localRepoDescriptorMap = centralConfig.getLocalRepositoriesMap();
        Map<String, LocalRepo> oldLocalRepos = null;
        if (oldDescriptor != null && globalVirtualRepo != null) {
            oldLocalRepos = globalVirtualRepo.getLocalRepositoriesMap();
        }
        for (LocalRepoDescriptor repoDescriptor : localRepoDescriptorMap.values()) {
            DbLocalRepo<LocalRepoDescriptor> oldLocalRepo = null;
            String key = repoDescriptor.getKey();
            if (oldLocalRepos != null) {
                LocalRepo oldRepo = oldLocalRepos.get(key);
                if (oldRepo != null) {
                    if (!(oldRepo instanceof DbLocalRepo)) {
                        log.error("Reloading configuration did not find local repository " + key);
                    } else {
                        //noinspection unchecked
                        oldLocalRepo = (DbLocalRepo<LocalRepoDescriptor>) oldRepo;
                    }
                } else {
                    // This could be a new repo that is in the newly saved config but not in the global map yet.
                    // Only if we do not find it there as well then it is an error
                    LocalRepoDescriptor newLocalRepo = centralConfig.getLocalRepositoriesMap().get(key);
                    if (newLocalRepo == null) {
                        log.error("Reloading configuration did not find local repository " + key);
                    }

                }
            }
            LocalRepo repo = new DbLocalRepo<>(repoDescriptor, transactionalMe, oldLocalRepo);
            try {
                repo.init();
            } catch (Exception e) {
                log.error("Failed to initialize local repository '{}'. Repository will be blacked-out",
                        repo.getKey(), e);
                ((LocalRepoDescriptor) repo.getDescriptor()).setBlackedOut(true);
            }
            localRepositoriesMap.put(repo.getKey(), repo);
        }

        //Remote repos
        Map<String, RemoteRepo> remoteRepositoriesMap = Maps.newLinkedHashMap();
        Map<String, RemoteRepoDescriptor> remoteRepoDescriptorMap = centralConfig.getRemoteRepositoriesMap();
        Map<String, RemoteRepo> oldRemoteRepos = null;
        if (oldDescriptor != null && globalVirtualRepo != null) {
            oldRemoteRepos = globalVirtualRepo.getRemoteRepositoriesMap();
        }
        NuGetAddon nuGetAddon = addonsManager.addonByType(NuGetAddon.class);
        for (RemoteRepoDescriptor repoDescriptor : remoteRepoDescriptorMap.values()) {
            RemoteRepo oldRemoteRepo = null;
            if (oldRemoteRepos != null) {
                oldRemoteRepo = oldRemoteRepos.get(repoDescriptor.getKey());
            }
            RemoteRepo repo = nuGetAddon.createRemoteRepo(transactionalMe, repoDescriptor,
                    centralConfig.isOfflineMode(), oldRemoteRepo);
            try {
                repo.init();
            } catch (Exception e) {
                log.error("Failed to initialize remote repository '" + repo.getKey() + "'. "
                        + "Repository will be blacked-out!", e);
                ((HttpRepoDescriptor) repo.getDescriptor()).setBlackedOut(true);
            }
            remoteRepositoriesMap.put(repo.getKey(), repo);
        }

        // create on-the-fly repo descriptor to be used by the global virtual repo
        List<RepoDescriptor> localAndRemoteRepoDescriptors = new ArrayList<>();
        localAndRemoteRepoDescriptors.addAll(localRepoDescriptorMap.values());
        localAndRemoteRepoDescriptors.addAll(remoteRepoDescriptorMap.values());
        VirtualRepoDescriptor vrd = new VirtualRepoDescriptor();
        vrd.setRepositories(localAndRemoteRepoDescriptors);
        vrd.setArtifactoryRequestsCanRetrieveRemoteArtifacts(
                ConstantValues.artifactoryRequestsToGlobalCanRetrieveRemoteArtifacts.getBoolean());
        vrd.setKey(VirtualRepoDescriptor.GLOBAL_VIRTUAL_REPO_KEY);
        // create and init the global virtual repo
        globalVirtualRepo = new VirtualRepo(vrd, transactionalMe, localRepositoriesMap, remoteRepositoriesMap);
        // no need to call globalVirtualRepo.init()
        globalVirtualRepo.initStorage();

        virtualRepositoriesMap.clear();// we rebuild the virtual repo cache
        virtualRepositoriesMap.put(globalVirtualRepo.getKey(), globalVirtualRepo);

        // virtual repos init in 2 passes
        Map<String, VirtualRepoDescriptor> virtualRepoDescriptorMap = centralConfig.getVirtualRepositoriesMap();
        // 1. create the virtual repos
        WebstartAddon webstartAddon = addonsManager.addonByType(WebstartAddon.class);
        for (VirtualRepoDescriptor repoDescriptor : virtualRepoDescriptorMap.values()) {
            VirtualRepo repo = webstartAddon.createVirtualRepo(transactionalMe, repoDescriptor);
            virtualRepositoriesMap.put(repo.getKey(), repo);
        }

        // 2. call the init method only after all virtual repos exist
        for (VirtualRepo virtualRepo : virtualRepositoriesMap.values()) {
            virtualRepo.init();
        }

        initAllRepoKeysCache();
    }

    @Override
    public List<ItemInfo> getChildrenDeeply(RepoPath path) {
        List<ItemInfo> result = Lists.newArrayList();
        if (path == null) {
            return result;
        }
        if (!hasChildren(path)) {
            return result;
        }
        List<ItemInfo> children = getChildren(path);
        for (ItemInfo child : children) {
            result.add(child);
            result.addAll(getChildrenDeeply(child.getRepoPath()));
        }
        return result;
    }

    @Override
    public ModuleInfo getItemModuleInfo(RepoPath repoPath) {
        Repo repo = assertRepoKey(repoPath);
        return repo.getItemModuleInfo(repoPath.getPath());
    }

    private ModuleInfo getDescriptorModuleInfo(RepoPath repoPath) {
        Repo repo = assertRepoKey(repoPath);
        return repo.getDescriptorModuleInfo(repoPath.getPath());
    }

    @Override
    public RepoPath getExplicitDescriptorPathByArtifact(RepoPath repoPath) {
        Repo repo = assertRepoKey(repoPath);

        RepoLayout repoLayout = repo.getDescriptor().getRepoLayout();
        if ((repoLayout == null) || !repoLayout.isDistinctiveDescriptorPathPattern()) {
            return repoPath;
        }

        ModuleInfo descriptorModuleInfo = getDescriptorModuleInfo(repoPath);
        if (descriptorModuleInfo.isValid()) {
            return repoPath;
        }

        ModuleInfo itemModuleInfo = getItemModuleInfo(repoPath);
        if (!itemModuleInfo.isValid()) {
            return repoPath;
        }

        String descriptorPath = ModuleInfoUtils.constructDescriptorPath(itemModuleInfo, repoLayout, true);
        return InternalRepoPathFactory.create(repoPath.getRepoKey(), descriptorPath);
    }

    private Repo assertRepoKey(RepoPath repoPath) {
        String repoKey = repoPath.getRepoKey();
        Repo repo = repositoryByKey(repoKey);
        if (repo == null) {
            throw new IllegalArgumentException("Repository '" + repoKey + "' not found!");
        }
        return repo;
    }

    @Override
    public boolean mkdirs(RepoPath folderRepoPath) {
        StoringRepo storingRepo = storingRepositoryByKey(folderRepoPath.getRepoKey());
        if (!storingRepo.itemExists(folderRepoPath.getPath())) {
            MutableVfsFolder folder = storingRepo.createOrGetFolder(folderRepoPath);
            return folder.isNew();
        }
        return false;
    }

    @Override
    public boolean virtualItemExists(RepoPath repoPath) {
        VirtualRepo virtualRepo = virtualRepositoryByKey(repoPath.getRepoKey());
        if (virtualRepo == null) {
            throw new RepositoryRuntimeException("Repository " + repoPath.getRepoKey() + " does not exists!");
        }
        return virtualRepo.virtualItemExists(repoPath.getPath());
    }

    @Override
    @Nonnull
    public MutableVfsItem getMutableItem(RepoPath repoPath) {
        //TORE: [by YS] should be storing repo once interfaces refactoring is done
        LocalRepo localRepo = localOrCachedRepositoryByKey(repoPath.getRepoKey());
        if (localRepo != null) {
            MutableVfsItem mutableFsItem = localRepo.getMutableFsItem(repoPath);
            if (mutableFsItem != null) {
                return mutableFsItem;
            }
        }
        throw new ItemNotFoundRuntimeException(repoPath);
    }

    private MutableVfsFile getMutableFile(RepoPath repoPath) {
        MutableVfsItem mutableItem = getMutableItem(repoPath);
        if (!(mutableItem instanceof MutableVfsFile)) {
            throw new FileExpectedException(repoPath);
        }
        return (MutableVfsFile) mutableItem;
    }

    @Override
    @Nullable
    public StatsInfo getStatsInfo(RepoPath repoPath) {
        if (!authService.canRead(repoPath)) {
            AccessLogger.downloadDenied(repoPath);
            return null;
        }
        return statsService.getStats(repoPath);
    }

    @Override
    public long getArtifactCount(RepoPath repoPath) {
        return fileService.getFilesCount(repoPath);
    }

    @Override
    public long getNodesCount(RepoPath repoPath) {
        return fileService.getNodesCount(repoPath);
    }

    @Override
    public List<FileInfo> searchFilesWithBadChecksum(ChecksumType type) {
        return fileService.searchFilesWithBadChecksum(type);
    }

    @Override
    @Nonnull
    public List<ItemInfo> getChildren(RepoPath repoPath) {
        TreeBrowsingCriteria criteria = new TreeBrowsingCriteriaBuilder().sortAlphabetically().applySecurity()
                .cacheChildren(false).build();
        ItemNode rootNode = new ItemTree(repoPath, criteria).getRootNode();
        if (rootNode != null) {
            return rootNode.getChildrenInfo();
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    public List<String> getChildrenNames(RepoPath repoPath) {
        List<ItemInfo> childrenInfo = getChildren(repoPath);
        List<String> childrenNames = Lists.newArrayListWithCapacity(childrenInfo.size());
        for (ItemInfo itemInfo : childrenInfo) {
            childrenNames.add(itemInfo.getName());
        }
        return childrenNames;
    }

    @Override
    public boolean hasChildren(RepoPath repoPath) {
        return fileService.hasChildren(repoPath);
    }

    @Override
    public VirtualRepo getGlobalVirtualRepo() {
        return globalVirtualRepo;
    }

    @Override
    public void saveFileInternal(RepoPath fileRepoPath, InputStream is) throws RepoRejectException, IOException {
        try {
            MutableFileInfo fileInfo = InfoFactoryHolder.get().createFileInfo(fileRepoPath);
            fileInfo.createTrustedChecksums();
            SaveResourceContext saveContext = new SaveResourceContext.Builder(new FileResource(fileInfo), is)
                    .build();
            StoringRepo storingRepo = storingRepositoryByKey(fileRepoPath.getRepoKey());
            if (storingRepo == null) {
                throw new IllegalArgumentException("Storing repo for '" + fileRepoPath + "' not found");
            }
            saveResource(storingRepo, saveContext);
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    @Override
    public List<VirtualRepo> getVirtualRepositories() {
        return new ArrayList<>(virtualRepositoriesMap.values());
    }

    @Override
    public List<LocalRepo> getLocalAndCachedRepositories() {
        return globalVirtualRepo.getLocalAndCachedRepositories();
    }

    @Override
    public List<RealRepo> getLocalAndRemoteRepositories() {
        return globalVirtualRepo.getLocalAndRemoteRepositories();
    }

    @Override
    public List<LocalRepoDescriptor> getLocalAndCachedRepoDescriptors() {
        List<LocalRepo> localAndCached = globalVirtualRepo.getLocalAndCachedRepositories();
        ArrayList<LocalRepoDescriptor> result = Lists.newArrayList();
        for (LocalRepo localRepo : localAndCached) {
            result.add((LocalRepoDescriptor) localRepo.getDescriptor());
        }
        return result;
    }

    @Override
    public List<RemoteRepoDescriptor> getRemoteRepoDescriptors() {
        List<RemoteRepo> remoteRepositories = globalVirtualRepo.getRemoteRepositories();
        ArrayList<RemoteRepoDescriptor> result = Lists.newArrayList();
        for (RemoteRepo remoteRepo : remoteRepositories) {
            result.add((RemoteRepoDescriptor) remoteRepo.getDescriptor());
        }
        return result;
    }

    @Override
    public VirtualRepoDescriptor virtualRepoDescriptorByKey(String repoKey) {
        if (repoKey == null || repoKey.length() == 0) {
            return null;
        }
        if (VirtualRepoDescriptor.GLOBAL_VIRTUAL_REPO_KEY.equals(repoKey)) {
            return globalVirtualRepo.getDescriptor();
        }
        return centralConfigService.getDescriptor().getVirtualRepositoriesMap().get(repoKey);
    }

    @Override
    public String getStringContent(FileInfo fileInfo) {
        return getStringContent(fileInfo.getRepoPath());
    }

    @Override
    public String getStringContent(RepoPath repoPath) {
        LocalRepo repo = localOrCachedRepositoryByKey(repoPath.getRepoKey());
        if (repo == null) {
            throw new IllegalArgumentException("Local repository for '" + repoPath + "' doesn't exist");
        }
        return repo.getTextFileContent(repoPath);
    }

    @Override
    public ResourceStreamHandle getResourceStreamHandle(RepoPath repoPath) {
        LocalRepo repo = localOrCachedRepositoryByKey(repoPath.getRepoKey());
        if (repo == null) {
            throw new IllegalArgumentException("Local repository for '" + repoPath + "' doesn't exist");
        }
        // Recreate the repo path for remote stream handle request
        if (repo.isCache() && !repo.getKey().equals(repoPath.getRepoKey())) {
            repoPath = InternalRepoPathFactory.cacheRepoPath(repoPath);
        }
        return repo.getFileContent(repoPath);
    }

    @Override
    public ArchiveFileContent getArchiveFileContent(RepoPath archivePath, String sourceEntryPath)
            throws IOException {
        LocalRepo repo = localOrCachedRepositoryByKey(archivePath.getRepoKey());
        return new ArchiveContentRetriever().getArchiveFileContent(repo, archivePath, sourceEntryPath);
    }

    @Override
    public ArchiveFileContent getGenericArchiveFileContent(RepoPath archivePath, String sourceEntryPath)
            throws IOException {
        LocalRepo repo = localOrCachedRepositoryByKey(archivePath.getRepoKey());
        return new ArchiveContentRetriever().getGenericArchiveFileContent(repo, archivePath, sourceEntryPath);
    }

    /**
     * Import all the repositories under the passed folder which matches local or cached repository declared in the
     * configuration. Having empty directory for each repository is allowed and not an error. Nothing will be imported
     * for those.
     */
    @Override
    public void importAll(ImportSettingsImpl settings) {
        RepositoryImportSettingsImpl repositoriesImportSettings = new RepositoryImportSettingsImpl(
                settings.getBaseDir(), settings);
        repositoriesImportSettings.setRepositories(getLocalAndCacheRepoKeys());
        repositoriesImportSettings.setRepositoriesToDelete(Collections.<String>emptyList());
        repositoriesImportSettings.setFailIfEmpty(false);
        importRepositoriesFromSettings(repositoriesImportSettings);
    }

    /**
     * Import the artifacts under the folder passed directly in the repository named "repoKey". If no repository with
     * this repo key exists or if the folder passed is empty, the status will be set to error.
     */
    @Override
    @SuppressWarnings({ "ThrowableInstanceNeverThrown" })
    public void importRepo(String repoKey, ImportSettingsImpl settings) {
        RepositoryImportSettingsImpl singleRepoImportSettings = new RepositoryImportSettingsImpl(
                settings.getBaseDir(), settings);
        singleRepoImportSettings.setRepositories(Lists.newArrayList(repoKey));
        singleRepoImportSettings.setSingleRepoImport(true);
        importRepositoriesFromSettings(singleRepoImportSettings);
    }

    /**
     * This method will delete and import all the local and cached repositories listed in the (newly loaded) config
     * file. This action is resource intensive and is done in multiple transactions to avoid out of memory exceptions.
     */
    @Override
    public void importFrom(ImportSettings settings) {
        MutableStatusHolder status = settings.getStatusHolder();
        File repoRootPath = getRepositoriesExportDir(settings.getBaseDir());
        if (!repoRootPath.exists() || !repoRootPath.isDirectory()) {
            if (settings.isFailIfEmpty()) {
                throw new IllegalArgumentException(
                        "Import root " + repoRootPath + " does not exist or not a directory");
            } else {
                status.status("No repositories root to import at " + repoRootPath, log);
                return;
            }
        }
        List<String> repositoryKeysForDeletion = getLocalAndCacheRepoKeys();
        List<String> localRepoKeysForImport = settings.getRepositories();
        if (localRepoKeysForImport.isEmpty()) {
            localRepoKeysForImport = new ArrayList<>(repositoryKeysForDeletion);
        }
        RepositoryImportSettingsImpl repositoriesImportSettings = new RepositoryImportSettingsImpl(repoRootPath,
                settings);
        repositoriesImportSettings.setRepositories(localRepoKeysForImport);
        repositoriesImportSettings.setRepositoriesToDelete(repositoryKeysForDeletion);
        repositoriesImportSettings.setFailIfEmpty(false);
        importRepositoriesFromSettings(repositoriesImportSettings);
    }

    private void importRepositoriesFromSettings(ImportSettingsImpl settings) {
        String jobToken = "No Job";
        boolean taskCompletion = false;
        MutableStatusHolder status = settings.getStatusHolder();
        try {
            jobToken = createAndStartImportJob(settings);
            taskCompletion = taskService.waitForTaskCompletion(jobToken);
        } finally {
            if (!taskCompletion && !status.isError()) {
                // Add error of no completion
                status.error("The task " + jobToken + " did not complete correctly.", log);
            }
        }
    }

    private String createAndStartImportJob(ImportSettingsImpl settings) {
        TaskBase task = TaskUtils.createManualTask(ImportJob.class, 0L);
        task.addAttribute(RepositoryImportSettingsImpl.class.getName(), settings);
        return taskService.startTask(task, true);
    }

    @Override
    public void exportTo(ExportSettings settings) {
        MutableStatusHolder status = settings.getStatusHolder();
        status.status("Exporting repositories...", log);
        if (TaskCallback.currentTaskToken() == null) {
            exportAsync(BaseSettings.FULL_SYSTEM, settings);
        } else {
            List<String> repoKeys = settings.getRepositories();
            for (String repoKey : repoKeys) {
                boolean stop = taskService.pauseOrBreak();
                if (stop) {
                    status.error("Export was stopped", log);
                    return;
                }
                exportRepo(repoKey, settings);
                if (status.isError() && settings.isFailFast()) {
                    return;
                }
            }

            if (settings.isIncremental()) {
                File repositoriesDir = getRepositoriesExportDir(settings.getBaseDir());
                cleanupIncrementalBackupDirectory(repositoriesDir, repoKeys);
            }
        }
    }

    @Override
    public void exportRepo(String repoKey, ExportSettings settings) {
        MutableStatusHolder status = settings.getStatusHolder();
        if (TaskCallback.currentTaskToken() == null) {
            exportAsync(repoKey, settings);
        } else {
            //Check if we need to break/pause
            boolean stop = taskService.pauseOrBreak();
            if (stop) {
                status.error("Export was stopped on " + repoKey, log);
                return;
            }
            LocalRepo sourceRepo = localOrCachedRepositoryByKey(repoKey);
            if (sourceRepo == null) {
                status.error("Export cannot be done on non existing repository " + repoKey, log);
                return;
            }
            File targetDir = getRepoExportDir(settings.getBaseDir(), repoKey);
            ExportSettingsImpl repoSettings = new ExportSettingsImpl(targetDir, settings);
            sourceRepo.exportTo(repoSettings);
        }
    }

    private File getRepoExportDir(File exportDir, String repoKey) {
        return new File(getRepositoriesExportDir(exportDir), repoKey);
    }

    private File getRepositoriesExportDir(File exportDir) {
        // the directory under the base export dir that contains the exported repositories
        return new File(exportDir, "repositories");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MutableStatusHolder exportSearchResults(SavedSearchResults searchResults,
            ExportSettingsImpl baseSettings) {
        return new DbRepoExportSearchHandler(searchResults, baseSettings).export();
    }

    @Override
    @Nonnull
    public ItemInfo getItemInfo(RepoPath repoPath) {
        LocalRepo localRepo = getLocalRepository(repoPath);
        VfsItem item = localRepo.getImmutableFsItem(repoPath);
        if (item != null) {
            return item.getInfo();
        }
        throw new ItemNotFoundRuntimeException("Item " + repoPath + " does not exist");
    }

    @Override
    @Nonnull
    public FileInfo getFileInfo(RepoPath repoPath) {
        ItemInfo itemInfo = getItemInfo(repoPath);
        if (itemInfo instanceof FileInfo) {
            return (FileInfo) itemInfo;
        } else {
            throw new FileExpectedException(repoPath);
        }
    }

    @Override
    @Nonnull
    public FolderInfo getFolderInfo(RepoPath repoPath) {
        ItemInfo itemInfo = getItemInfo(repoPath);
        if (itemInfo instanceof FolderInfo) {
            return (FolderInfo) itemInfo;
        } else {
            throw new FolderExpectedException(repoPath);
        }
    }

    @Override
    public boolean exists(RepoPath repoPath) {
        String repoKey = repoPath.getRepoKey();
        LocalRepo localRepo = localOrCachedRepositoryByKey(repoKey);
        return localRepo != null && localRepo.itemExists(repoPath.getPath());
    }

    @Override
    public ItemMetaInfo getItemMetaInfo(RepoPath repoPath) {
        return ContextHelper.get().beanForType(NodeMetaInfoService.class).getNodeMetaInfo(repoPath);
    }

    @Override
    public boolean hasProperties(RepoPath repoPath) {
        MutableVfsItem mutableSessionItem = LockingHelper.getIfWriteLockedByMe(repoPath);
        if (mutableSessionItem != null) {
            return mutableSessionItem.getProperties().isEmpty();
        } else {
            return ContextHelper.get().beanForType(PropertiesService.class).hasProperties(repoPath);
        }
    }

    @Override
    @Nullable
    public Properties getProperties(RepoPath repoPath) {
        if (!authService.canRead(repoPath)) {
            AccessLogger.downloadDenied(repoPath);
            return null;
        }

        MutableVfsItem mutableItem = LockingHelper.getIfWriteLockedByMe(repoPath);
        if (mutableItem != null) {
            return mutableItem.getProperties();
        } else {
            return ContextHelper.get().beanForType(PropertiesService.class).getProperties(repoPath);
        }
    }

    @Override
    public boolean setProperties(RepoPath repoPath, Properties properties) {
        if (!assertCanAnnotate(repoPath, "Properties")) {
            return false;
        }
        LocalRepo repository = getLocalRepository(repoPath);
        MutableVfsItem mutableItem = repository.getMutableFsItem(repoPath);
        if (mutableItem == null) {
            log.warn("Cannot set properties on '{}': Item not found.", repoPath);
            return false;
        }

        mutableItem.setProperties(properties);

        ReplicationAddon replicationAddon = addonsManager.addonByType(ReplicationAddon.class);
        replicationAddon.offerLocalReplicationPropertiesChangeEvent(repoPath);
        return true;
    }

    @Override
    public boolean removeProperties(RepoPath repoPath) {
        return setProperties(repoPath, new PropertiesImpl());
    }

    private boolean assertCanAnnotate(RepoPath repoPath, String metadataName) {
        if (!authService.canAnnotate(repoPath)) {
            AccessLogger.annotateDenied(repoPath);
            log.error("Cannot set '{}' on '{}': lacking annotate permissions.", metadataName, repoPath.getId());
            return false;
        }
        return true;
    }

    @Override
    public MoveMultiStatusHolder moveWithoutMavenMetadata(RepoPath from, RepoPath to, boolean dryRun,
            boolean suppressLayouts, boolean failFast) {
        MoverConfigBuilder configBuilder = new MoverConfigBuilder(from, to).copy(false).dryRun(dryRun)
                .executeMavenMetadataCalculation(false).atomic(true).suppressLayouts(suppressLayouts)
                .failFast(failFast);
        return moveOrCopy(configBuilder.build());
    }

    @Override
    public ChecksumInfo setClientChecksum(LocalRepo repo, ChecksumType checksumType, RepoPath targetFileRepoPath,
            String checksum) {
        MutableVfsItem fsItem = repo.getMutableFsItem(targetFileRepoPath);
        if (fsItem == null) {
            throw new ItemNotFoundRuntimeException(targetFileRepoPath);
        }
        if (!fsItem.isFile()) {
            throw new FileExpectedException(targetFileRepoPath);
        }

        if (!checksumType.isValid(checksum)) {
            log.warn("Uploading non valid original checksum for {}", fsItem.getRepoPath());
        }
        MutableVfsFile vfsFile = (MutableVfsFile) fsItem;
        vfsFile.setClientChecksum(checksumType, checksum);

        // fire replication event for the client checksum
        RepoPath checksumRepoPath = InternalRepoPathFactory.create(targetFileRepoPath.getRepoKey(),
                targetFileRepoPath.getPath() + checksumType.ext());
        addonsManager.addonByType(ReplicationAddon.class).offerLocalReplicationDeploymentEvent(checksumRepoPath);

        return vfsFile.getInfo().getChecksumsInfo().getChecksumInfo(checksumType);
    }

    @Override
    public MoveMultiStatusHolder move(RepoPath from, RepoPath to, boolean dryRun, boolean suppressLayouts,
            boolean failFast) {
        MoverConfigBuilder configBuilder = new MoverConfigBuilder(from, to).copy(false).dryRun(dryRun)
                .executeMavenMetadataCalculation(true).suppressLayouts(suppressLayouts).failFast(failFast)
                .atomic(true);
        return moveOrCopy(configBuilder.build());
    }

    @Override
    public MoveMultiStatusHolder move(Set<RepoPath> pathsToMove, String targetLocalRepoKey, Properties properties,
            boolean dryRun, boolean failFast) {
        Set<RepoPath> pathsToMoveIncludingParents = aggregatePathsToMove(pathsToMove, targetLocalRepoKey, false);

        log.debug("The following paths will be moved: {}", pathsToMoveIncludingParents);
        // start moving each path separately, marking each folder or file's parent folder for metadata recalculation
        MoveMultiStatusHolder status = new MoveMultiStatusHolder();
        RepoPathMover mover = getMoveRepoPathService();
        for (RepoPath pathToMove : pathsToMoveIncludingParents) {
            RepoPath targetRepoPath = InternalRepoPathFactory.create(targetLocalRepoKey, pathToMove.getPath());
            log.debug("Moving path: {} to {}", pathToMove, targetRepoPath);
            mover.executeOperation(status,
                    new MoverConfigBuilder(pathToMove, targetRepoPath).copy(false).dryRun(dryRun)
                            .executeMavenMetadataCalculation(false).pruneEmptyFolders(true).properties(properties)
                            .unixStyleBehavior(false).failFast(failFast).atomic(true).build());
        }
        mavenMetadataService
                .calculateMavenMetadataAsyncNonRecursive(status.getCandidatesForMavenMetadataCalculation());
        return status;
    }

    @Override
    public MoveMultiStatusHolder copyMultiTx(RepoPath fromRepoPath, RepoPath targetRepoPath, boolean dryRun,
            boolean suppressLayouts, boolean failFast) {
        MoverConfigBuilder configBuilder = new MoverConfigBuilder(fromRepoPath, targetRepoPath).copy(true)
                .dryRun(dryRun).executeMavenMetadataCalculation(true).suppressLayouts(suppressLayouts)
                .failFast(failFast);
        return moveOrCopy(configBuilder.build());
    }

    @Override
    public MoveMultiStatusHolder moveMultiTx(RepoPath from, RepoPath to, boolean dryRun, boolean suppressLayouts,
            boolean failFast) {
        MoverConfigBuilder configBuilder = new MoverConfigBuilder(from, to).copy(false).dryRun(dryRun)
                .executeMavenMetadataCalculation(true).suppressLayouts(suppressLayouts).failFast(failFast);
        return moveOrCopy(configBuilder.build());
    }

    @Override
    public MoveMultiStatusHolder copy(RepoPath fromRepoPath, RepoPath targetRepoPath, boolean dryRun,
            boolean suppressLayouts, boolean failFast) {
        MoverConfigBuilder configBuilder = new MoverConfigBuilder(fromRepoPath, targetRepoPath).copy(true)
                .dryRun(dryRun).executeMavenMetadataCalculation(true).suppressLayouts(suppressLayouts)
                .failFast(failFast);
        return moveOrCopy(configBuilder.atomic(true).build());
    }

    @Override
    public MoveMultiStatusHolder copy(Set<RepoPath> pathsToCopy, String targetLocalRepoKey, Properties properties,
            boolean dryRun, boolean failFast) {
        Set<RepoPath> pathsToCopyIncludingParents = aggregatePathsToMove(pathsToCopy, targetLocalRepoKey, true);

        log.debug("The following paths will be copied: {}", pathsToCopyIncludingParents);
        //Start copying each path separately, marking each folder or file's parent folder for metadata recalculation
        MoveMultiStatusHolder status = new MoveMultiStatusHolder();
        RepoPathMover mover = getCopyRepoPathService();
        for (RepoPath pathToCopy : pathsToCopyIncludingParents) {
            RepoPath targetRepoPath = InternalRepoPathFactory.create(targetLocalRepoKey, pathToCopy.getPath());
            log.debug("Copying path: {} to {}", pathToCopy, targetRepoPath);
            mover.executeOperation(status,
                    new MoverConfigBuilder(pathToCopy, targetRepoPath).copy(true).dryRun(dryRun)
                            .executeMavenMetadataCalculation(false).pruneEmptyFolders(false).properties(properties)
                            .unixStyleBehavior(false).failFast(failFast).atomic(true).build());
        }
        mavenMetadataService
                .calculateMavenMetadataAsyncNonRecursive(status.getCandidatesForMavenMetadataCalculation());

        return status;
    }

    private MoveMultiStatusHolder moveOrCopy(MoverConfig config) {
        MoveMultiStatusHolder status = new MoveMultiStatusHolder();
        // copy or move service
        if (config.isCopy()) {
            getCopyRepoPathService().executeOperation(status, config);
        } else {
            getMoveRepoPathService().executeOperation(status, config);
        }
        return status;
    }

    /**
     * Returns an instance of the Repo Path Mover
     *
     * @return RepoPathMover
     */
    private RepoPathMover getMoveRepoPathService() {
        return ContextHelper.get().beanForType(MoveRepoPathService.class);
    }

    /**
     * Returns an instance of the Repo Path Mover
     *
     * @return RepoPathMover
     */
    private RepoPathMover getCopyRepoPathService() {
        return ContextHelper.get().beanForType(CopyRepoPathService.class);
    }

    @Override
    public StatusHolder deploy(RepoPath repoPath, InputStream inputStream) {
        try {
            ArtifactoryDeployRequest request = new ArtifactoryDeployRequestBuilder(repoPath)
                    .inputStream(inputStream).build();
            InternalArtifactoryResponse response = new InternalArtifactoryResponse();
            uploadService.upload(request, response);
            return response.getStatusHolder();
        } catch (Exception e) {
            String msg = String.format("Cannot deploy to '{%s}'.", repoPath);
            log.debug(msg, e);
            throw new RepositoryRuntimeException(msg, e);
        }
    }

    @Override
    public FileInfo getVirtualFileInfo(RepoPath virtualRepoPath) {
        VirtualRepo virtualRepo = virtualRepositoryByKey(virtualRepoPath.getRepoKey());
        if (virtualRepo == null) {
            throw new IllegalArgumentException(virtualRepoPath.getRepoKey() + " is not a virtual repository.");
        }
        Set<LocalRepo> resolvedLocalRepos = virtualRepo.getResolvedLocalAndCachedRepos();
        for (LocalRepo resolvedLocalRepo : resolvedLocalRepos) {
            if (resolvedLocalRepo.itemExists(virtualRepoPath.getPath())) {
                return getFileInfo(resolvedLocalRepo.getRepoPath(virtualRepoPath.getPath()));
            }
        }

        throw new ItemNotFoundRuntimeException("Item " + virtualRepoPath + " does not exists");
    }

    @Override
    public ItemInfo getVirtualItemInfo(RepoPath virtualRepoPath) {
        VirtualRepo virtualRepo = virtualRepositoryByKey(virtualRepoPath.getRepoKey());
        if (virtualRepo == null) {
            throw new IllegalArgumentException(virtualRepoPath.getRepoKey() + " is not a virtual repository.");
        }
        Set<LocalRepo> resolvedLocalRepos = virtualRepo.getResolvedLocalAndCachedRepos();
        for (LocalRepo resolvedLocalRepo : resolvedLocalRepos) {
            if (resolvedLocalRepo.itemExists(virtualRepoPath.getPath())) {
                return getItemInfo(resolvedLocalRepo.getRepoPath(virtualRepoPath.getPath()));
            }
        }

        throw new ItemNotFoundRuntimeException("Item " + virtualRepoPath + " does not exists");
    }

    @Override
    public BasicStatusHolder undeploy(RepoPath repoPath, boolean calcMavenMetadata) {
        return undeploy(repoPath, calcMavenMetadata, false);
    }

    @Override
    public BasicStatusHolder undeploy(RepoPath repoPath, boolean calcMavenMetadata, boolean pruneEmptyFolders) {
        String repoKey = repoPath.getRepoKey();
        StoringRepo storingRepo = storingRepositoryByKey(repoKey);
        BasicStatusHolder statusHolder = new BasicStatusHolder();
        if (storingRepo == null) {
            statusHolder.error("Could find storing repository by key '" + repoKey + "'", log);
            return statusHolder;
        }
        PathDeletionContext pathDeletionContext = new PathDeletionContext.Builder(storingRepo, repoPath.getPath(),
                statusHolder).assertOverwrite(false).build();
        assertDelete(pathDeletionContext);
        if (!statusHolder.isError()) {
            try {
                storingRepo.undeploy(repoPath, calcMavenMetadata);
            } catch (CancelException e) {
                statusHolder.error("Undeploy was canceled by user plugin", e.getErrorCode(), e, log);
            }
        }

        if (pruneEmptyFolders && !repoPath.isRoot()) {
            pruneService.prune(repoPath.getParent());
        }

        return statusHolder;
    }

    @Override
    public BasicStatusHolder undeployMultiTransaction(RepoPath repoPath) {
        String repoKey = repoPath.getRepoKey();
        StoringRepo storingRepo = storingRepositoryByKey(repoKey);
        BasicStatusHolder statusHolder = new BasicStatusHolder();
        if (storingRepo == null) {
            statusHolder.error("Could find storing repository by key '" + repoKey + "'", log);
            return statusHolder;
        }
        PathDeletionContext pathDeletionContext = new PathDeletionContext.Builder(storingRepo, repoPath.getPath(),
                statusHolder).assertOverwrite(false).build();
        assertDelete(pathDeletionContext);
        if (!statusHolder.isError()) {
            ItemInfo itemInfo = getItemInfo(repoPath);
            return undeploySingleItemTransactions(itemInfo.getRepoPath(), true, statusHolder);
        }
        return statusHolder;
    }

    /**
     * delete single items leaf only by single trx for each item
     *
     * @param repoPath          - repo path
     * @param calcMavenMetadata - if true calculate meta data
     * @return
     */
    private BasicStatusHolder undeploySingleItemTransactions(RepoPath repoPath, boolean calcMavenMetadata,
            BasicStatusHolder statusHolder) {
        BasicStatusHolder deleteItemStatusHolder;
        if (repoPath.isFile()) {
            deleteItemStatusHolder = deleteSingleLeaf(repoPath, calcMavenMetadata, statusHolder);
        } else {// folder
            deleteItemStatusHolder = deleteFoldersLeafsBySingleTrx(repoPath, calcMavenMetadata, statusHolder);
        }
        return deleteItemStatusHolder;
    }

    /**
     * iterate folder leaf and delete is as single trx
     *
     * @param repoPath - folder repo path
     */
    private BasicStatusHolder deleteFoldersLeafsBySingleTrx(RepoPath repoPath, boolean calcMavenMetadata,
            BasicStatusHolder statusHolder) {
        FileService fileService = ContextHelper.get().beanForType(FileService.class);
        List<ItemInfo> children = fileService.loadChildren(repoPath);
        //delete folder children
        for (ItemInfo child : children) {
            RepoPath childRepoPath = child.getRepoPath();
            undeploySingleItemTransactions(childRepoPath, false, statusHolder);
        }
        // delete empty folder
        return deleteSingleLeaf(repoPath, calcMavenMetadata, statusHolder);
    }

    /**
     * delete leaf as single trx
     *
     * @param repoPath          - file ((leaf) repo path
     * @param calcMavenMetadata
     */
    private BasicStatusHolder deleteSingleLeaf(RepoPath repoPath, boolean calcMavenMetadata,
            BasicStatusHolder statusHolder) {
        LockableUndeploy lockableUndeploy = InternalContextHelper.get().beanForType(LockableUndeploy.class);
        return lockableUndeploy.undeployInternal(repoPath, calcMavenMetadata, statusHolder);
    }

    @Override
    public BasicStatusHolder undeployInternal(RepoPath repoPath, boolean calcMavenMetadata,
            BasicStatusHolder statusHolder) {
        StoringRepo storingRepo = storingRepositoryByKey(repoPath.getRepoKey());
        try {
            storingRepo.undeploy(repoPath, calcMavenMetadata);
        } catch (CancelException e) {
            statusHolder.error("Undeploy was canceled by user plugin", e.getErrorCode(), e, log);
        }
        return statusHolder;
    }

    @Override
    public BasicStatusHolder undeploy(RepoPath repoPath) {
        return undeploy(repoPath, true);
    }

    @Override
    public StatusHolder undeployVersionUnits(Set<VersionUnit> versionUnits) {
        BasicStatusHolder status = new BasicStatusHolder();
        InternalRepositoryService transactionalMe = getTransactionalMe();

        Set<RepoPath> pathsForMavenMetadataCalculation = Sets.newHashSet();

        for (VersionUnit versionUnit : versionUnits) {
            Set<RepoPath> repoPaths = versionUnit.getRepoPaths();
            if (repoPaths.stream().filter(authService::canDelete).count() != repoPaths.size()) {
                status.warn(
                        "User " + authService.currentUsername() + " doesn't have permission to delete one or more "
                                + "the paths associated with module '"
                                + versionUnit.getModuleInfo().getPrettyModuleId() + "', it will not be removed.",
                        log);
                continue;
            }
            for (RepoPath repoPath : repoPaths) {
                BasicStatusHolder holder = transactionalMe.undeploy(repoPath, false, true);
                status.merge(holder);
                if (NamingUtils.isPom(repoPath.getPath())) {
                    // We need to re-calculate the artifact id folder (which is the grandparent of the pom file)
                    RepoPath grandparentFolder = RepoPathUtils.getAncestor(repoPath, 2);
                    if (grandparentFolder != null) {
                        pathsForMavenMetadataCalculation.add(grandparentFolder);
                    }
                }
            }
        }
        // Check to make sure of existence, might have been removed through the iterations of the version units
        pathsForMavenMetadataCalculation.stream().filter(this::exists)
                .forEach(path -> mavenMetadataService.calculateMavenMetadataAsync(path, true));
        return status;
    }

    @Override
    public int zap(RepoPath repoPath) {
        int zappedItems = 0;
        LocalRepo localRepo = getLocalRepository(repoPath);
        if (localRepo.isCache()) {
            LocalCacheRepo cache = (LocalCacheRepo) localRepo;
            zappedItems = cache.zap(repoPath);
        } else {
            log.warn("Got a zap request on a non-local-cache node '" + repoPath + "'.");
        }
        return zappedItems;
    }

    @Override
    public List<FolderInfo> getWithEmptyChildren(FolderInfo folderInfo) {
        FolderCompactor compactor = ContextHelper.get().beanForType(FolderCompactor.class);
        return compactor.getFolderWithCompactedChildren(folderInfo);
    }

    @Override
    public Set<String> getAllRepoKeys() {
        return allRepoKeysCache;
    }

    @Override
    public List<RepoDescriptor> getLocalAndRemoteRepoDescriptors() {
        return globalVirtualRepo.getDescriptor().getRepositories();
    }

    @Override
    public boolean isAnonAccessEnabled() {
        return authService.isAnonAccessEnabled();
    }

    @Override
    public Repo repositoryByKey(String key) {
        Repo repo = null;
        if (globalVirtualRepo.getLocalRepositoriesMap().containsKey(key)) {
            repo = globalVirtualRepo.localRepositoryByKey(key);
        } else if (globalVirtualRepo.getLocalCacheRepositoriesMap().containsKey(key)) {
            repo = globalVirtualRepo.getLocalCacheRepositoriesMap().get(key);
        } else if (globalVirtualRepo.getRemoteRepositoriesMap().containsKey(key)) {
            repo = globalVirtualRepo.getRemoteRepositoriesMap().get(key);
        } else if (virtualRepositoriesMap.containsKey(key)) {
            repo = virtualRepositoriesMap.get(key);
        } else if (globalVirtualRepo.getKey().equals(key)) {
            repo = globalVirtualRepo;
        }
        return repo;
    }

    @Override
    public LocalRepo localRepositoryByKey(String key) {
        return globalVirtualRepo.localRepositoryByKey(key);
    }

    @Override
    public RemoteRepo remoteRepositoryByKey(String key) {
        return globalVirtualRepo.remoteRepositoryByKey(key);
    }

    @Override
    public VirtualRepo virtualRepositoryByKey(String key) {
        return virtualRepositoriesMap.get(key);
    }

    @Override
    @Nullable
    public LocalRepo localOrCachedRepositoryByKey(String key) {
        return globalVirtualRepo.localOrCachedRepositoryByKey(key);
    }

    @Override
    public RealRepo localOrRemoteRepositoryByKey(String key) {
        return globalVirtualRepo.localOrRemoteRepositoryByKey(key);
    }

    @Override
    @SuppressWarnings({ "unchecked" })
    public <R extends Repo> RepoRepoPath<R> getRepoRepoPath(RepoPath repoPath) {
        String repoKey = repoPath.getRepoKey();
        R repo = (R) repositoryByKey(repoKey);
        if (repo == null) {
            throw new IllegalArgumentException("Repository '" + repoKey + "' not found!");
        }
        RepoRepoPath<R> rrp = new RepoRepoPath<>(repo, repoPath);
        return rrp;
    }

    @Override
    public StoringRepo storingRepositoryByKey(String key) {
        LocalRepo localRepo = localOrCachedRepositoryByKey(key);
        if (localRepo != null) {
            return localRepo;
        } else {
            return virtualRepositoryByKey(key);
        }
    }

    @Override
    public boolean isWriteLocked(RepoPath repoPath) {
        StoringRepo storingRepo = storingRepositoryByKey(repoPath.getRepoKey());
        if (storingRepo != null) {
            return storingRepo.isWriteLocked(repoPath);
        }
        return false;
    }

    @Override
    public List<ItemInfo> getOrphanItems(RepoPath repoPath) {
        return fileService.getOrphanItems(repoPath);
    }

    @Override
    public List<LocalRepoDescriptor> getLocalRepoDescriptors() {
        return new ArrayList<>(centralConfigService.getDescriptor().getLocalRepositoriesMap().values());
    }

    @Override
    public List<LocalCacheRepoDescriptor> getCachedRepoDescriptors() {
        List<LocalCacheRepo> localAndCached = globalVirtualRepo.getLocalCaches();
        List<LocalCacheRepoDescriptor> result = new ArrayList<>();
        for (LocalRepo localRepo : localAndCached) {
            result.add((LocalCacheRepoDescriptor) localRepo.getDescriptor());
        }
        return result;
    }

    @Override
    public RepoDescriptor repoDescriptorByKey(String key) {
        Repo repo = globalVirtualRepo.repositoryByKey(key);
        if (repo != null) {
            return repo.getDescriptor();
        }
        return null;
    }

    @Override
    public LocalRepoDescriptor localRepoDescriptorByKey(String key) {
        LocalRepo localRepo = globalVirtualRepo.localRepositoryByKey(key);
        if (localRepo != null) {
            return (LocalRepoDescriptor) localRepo.getDescriptor();
        }
        return null;
    }

    @Override
    public LocalRepoDescriptor localOrCachedRepoDescriptorByKey(String key) {
        LocalRepo localRepo = globalVirtualRepo.localOrCachedRepositoryByKey(key);
        if (localRepo != null) {
            return (LocalRepoDescriptor) localRepo.getDescriptor();
        }
        return null;
    }

    @Override
    public RemoteRepoDescriptor remoteRepoDescriptorByKey(String key) {
        RemoteRepo remoteRepo = globalVirtualRepo.remoteRepositoryByKey(key);
        if (remoteRepo != null) {
            return (RemoteRepoDescriptor) remoteRepo.getDescriptor();
        }
        return null;
    }

    @Override
    public List<VirtualRepoDescriptor> getVirtualRepoDescriptors() {
        ArrayList<VirtualRepoDescriptor> list = new ArrayList<>();
        if (!ConstantValues.disableGlobalRepoAccess.getBoolean()) {
            list.add(globalVirtualRepo.getDescriptor());
        }
        list.addAll(centralConfigService.getDescriptor().getVirtualRepositoriesMap().values());
        return list;
    }

    @Override
    public Repo nonCacheRepositoryByKey(String key) {
        Repo repo = globalVirtualRepo.nonCacheRepositoryByKey(key);
        if (repo == null) {
            repo = virtualRepositoriesMap.get(key);
        }
        assert repo != null;
        return repo;
    }

    @Override
    public boolean isRemoteAssumedOffline(@Nonnull String remoteRepoKey) {
        RemoteRepo remoteRepo = remoteRepositoryByKey(remoteRepoKey);
        if (remoteRepo == null) {
            return false;
        }
        return remoteRepo.isAssumedOffline();
    }

    @Override
    public long getRemoteNextOnlineCheck(String remoteRepoKey) {
        RemoteRepo remoteRepo = remoteRepositoryByKey(remoteRepoKey);
        if (remoteRepo == null) {
            return 0;
        }
        return remoteRepo.getNextOnlineCheckMillis();
    }

    @Override
    public void resetAssumedOffline(String remoteRepoKey) {
        RemoteRepo remoteRepo = remoteRepositoryByKey(remoteRepoKey);
        if (remoteRepo != null) {
            remoteRepo.resetAssumedOffline();
        }
    }

    @Override
    public void assertValidDeployPath(ValidDeployPathContext validDeployPathContext) throws RepoRejectException {
        LocalRepo repo = validDeployPathContext.getRepo();
        RepoPath repoPath = validDeployPathContext.getRepoPath();
        String path = repoPath.getPath();
        if (!repo.getKey().equals(repoPath.getRepoKey())) {
            // the repo path should point to the given repo (e.g, in case the repo path points to the remote repo)
            repoPath = InternalRepoPathFactory.create(repo.getKey(), path, repoPath.isFolder());
        }
        BasicStatusHolder status = repo.assertValidPath(repoPath, false);
        if (!status.isError()) {
            // if it is metadata, assert annotate privileges. Maven metadata is treated as regular file
            // (needs deploy permissions).
            if (NamingUtils.isMetadata(path)) {
                if (!authService.canAnnotate(repoPath)) {
                    String msg = "User " + authService.currentUsername() + " is not permitted to annotate '" + path
                            + "' on '" + repoPath + "'.";
                    status.error(msg, HttpStatus.SC_FORBIDDEN, log);
                    AccessLogger.annotateDenied(repoPath);
                }
            } else {
                //Assert deploy privileges
                boolean canDeploy = authService.canDeploy(repoPath);
                if (!canDeploy) {
                    String msg = "User " + authService.currentUsername() + " is not permitted to deploy '" + path
                            + "' into '" + repoPath + "'.";
                    status.error(msg, HttpStatus.SC_FORBIDDEN, log);
                    AccessLogger.deployDenied(repoPath);
                }
            }
            if (!status.isError()) {
                PathDeletionContext pathDeletionContext = new PathDeletionContext.Builder(repo, path, status)
                        .assertOverwrite(true).requestSha1(validDeployPathContext.getRequestSha1())
                        .forceExpiryCheck(validDeployPathContext.isForceExpiryCheck()).build();
                assertDelete(pathDeletionContext);
            }

            if (!status.isError()) {
                // Assert that we don't exceed the user configured maximum storage size
                assertStorageQuota(status, validDeployPathContext.getContentLength());
            }
        }
        if (status.isError()) {
            if (status.getException() != null) {
                Throwable throwable = status.getException();
                if (throwable instanceof RepoRejectException) {
                    throw (RepoRejectException) throwable;
                }
                throw new RepoRejectException(throwable);
            }
            throw new RepoRejectException(status.getStatusMsg(), status.getStatusCode());
        }
    }

    private void assertStorageQuota(MutableStatusHolder statusHolder, long contentLength) {
        StorageQuotaInfo info = storageService.getStorageQuotaInfo(contentLength);
        if (info == null) {
            return;
        }

        if (info.isLimitReached()) {
            // Note: don't display the disk usage in the status holder - this message is written back to the user
            statusHolder.error(
                    "Datastore disk usage is too high. Contact your Artifactory administrator to add additional "
                            + "storage space or change the disk quota limits.",
                    HttpStatus.SC_REQUEST_TOO_LONG, log);

            log.error(info.getErrorMessage());
        } else if (info.isWarningLimitReached()) {
            log.warn(info.getWarningMessage());
        }
    }

    @Override
    public <T extends RemoteRepoDescriptor> ResourceStreamHandle downloadAndSave(
            InternalRequestContext requestContext, RemoteRepo<T> remoteRepo, RepoResource res)
            throws IOException, RepoRejectException {
        return remoteRepo.downloadAndSave(requestContext, res);
    }

    @Override
    public RepoResource unexpireIfExists(LocalRepo localCacheRepo, String path) {
        RepoResource resource = internalUnexpireIfExists(localCacheRepo, path);
        if (resource == null) {
            return new UnfoundRepoResource(InternalRepoPathFactory.create(localCacheRepo.getKey(), path),
                    "Object is not in cache");
        }
        return resource;
    }

    @Override
    public ResourceStreamHandle unexpireAndRetrieveIfExists(InternalRequestContext requestContext,
            LocalRepo localCacheRepo, String path) throws IOException, RepoRejectException {
        RepoResource resource = internalUnexpireIfExists(localCacheRepo, path);
        if (resource != null && resource.isFound()) {
            return localCacheRepo.getResourceStreamHandle(requestContext, resource);
        }
        return null;
    }

    @Override
    public ResourceStreamHandle getResourceStreamHandle(InternalRequestContext requestContext, Repo repo,
            RepoResource res) throws IOException, RepoRejectException {
        if (res instanceof ResolvedResource) {
            RepoRequests
                    .logToContext("The requested resource is already resolved - using a string resource handle");
            // resource already contains the content - just extract it and return a string resource handle
            String content = ((ResolvedResource) res).getContent();
            return new StringResourceStreamHandle(content);
        } else {
            RepoRequests.logToContext("The requested resource isn't pre-resolved");
            RepoPath repoPath = res.getRepoPath();
            if (repo.isReal()) {
                RepoRequests
                        .logToContext("Target repository isn't virtual - verifying that downloading is allowed");
                //Permissions apply only to real repos
                StatusHolder holder = ((RealRepo) repo).checkDownloadIsAllowed(repoPath);
                if (holder.isError()) {
                    RepoRequests.logToContext("Download isn't allowed - received status {} and message '%s'",
                            holder.getStatusCode(), holder.getStatusMsg());
                    throw new RepoRejectException(holder.getStatusMsg(), holder.getStatusCode());
                }
            }
            return repo.getResourceStreamHandle(requestContext, res);
        }
    }

    @Override
    public RepoResource saveResource(StoringRepo repo, SaveResourceContext saveContext)
            throws IOException, RepoRejectException {
        // save binary early without opening DB transaction (except in full db mode)
        SaveResourceContext newSaveContext = null;
        try {

            BinaryInfo binaryInfo = binaryStore.addBinary(saveContext.getInputStream());
            newSaveContext = new SaveResourceContext.Builder(saveContext).binaryInfo(binaryInfo).build();
        } catch (IOException e) {
            saveContext.setException(e); // signal error
            throw e;
        }
        return getTransactionalMe().saveResourceInTransaction(repo, newSaveContext);
    }

    public String getSha1BySha2Property(String value) {
        PropertySearchControls propertyControlSearch = getPropertyControlSearch("sha256", value);
        ItemSearchResults<PropertySearchResult> searchResults = searchService
                .searchPropertyAql(propertyControlSearch);
        if (!searchResults.getResults().isEmpty()) {
            PropertySearchResult propertySearchResult = searchResults.getResults().get(0);
            FileInfo itemInfo = (FileInfo) propertySearchResult.getItemInfo();
            if (itemInfo != null) {
                return itemInfo.getSha1();
            }
        }
        return null;
    }

    /**
     * update property control search
     *
     * @return property control search
     */
    private PropertySearchControls getPropertyControlSearch(String key, String value) {
        PropertySearchControls propertySearchControls = new PropertySearchControls();
        propertySearchControls.setSelectedRepoForSearch(new ArrayList<>());
        propertySearchControls.setLimitSearchResults(true);
        propertySearchControls.put(key, value, true);
        return propertySearchControls;
    }

    @Override
    public RepoResource saveResourceInTransaction(StoringRepo repo, SaveResourceContext saveContext)
            throws IOException, RepoRejectException {
        return repo.saveResource(saveContext);
    }

    @Override
    public VersionSearchResults getVersionUnitsUnder(RepoPath repoPath) {
        VersionUnitSearchControls controls = new VersionUnitSearchControls(repoPath);
        return searchService.searchVersionUnits(controls);
    }

    @Override
    public long getArtifactCount() throws RepositoryRuntimeException {
        if (artifactCountRetriever == null) {
            artifactCountRetriever = new ArtifactCountRetriever();
        }
        return artifactCountRetriever.getCount();
    }

    @Override
    public List<VirtualRepoDescriptor> getVirtualReposContainingRepo(RepoDescriptor repoDescriptor) {
        RepoDescriptor descriptor = repoDescriptor;
        if (repoDescriptor instanceof LocalCacheRepoDescriptor) {
            //VirtualRepoResolver does not directly support local cache repos, so if the items descriptor is a cache,
            //We extract the caches remote repo, and use it instead
            descriptor = ((LocalCacheRepoDescriptor) repoDescriptor).getRemoteRepo();
        }

        List<VirtualRepoDescriptor> reposToDisplay = new ArrayList<>();
        List<VirtualRepoDescriptor> virtualRepos = getVirtualRepoDescriptors();
        for (VirtualRepoDescriptor virtualRepo : virtualRepos) {
            VirtualRepoResolver resolver = new VirtualRepoResolver(virtualRepo);
            if (resolver.contains(descriptor)) {
                reposToDisplay.add(virtualRepo);
            }
        }
        return reposToDisplay;
    }

    /**
     * Returns a list of local (non-cache) repo descriptors that the current user is permitted to deploy to.
     *
     * @return List of deploy-permitted local repos
     */
    @Override
    public List<LocalRepoDescriptor> getDeployableRepoDescriptors() {
        // if the user is an admin user, simply return all the deployable descriptors without checking specific
        // permission targets.
        if (authService.isAdmin()) {
            return getLocalRepoDescriptors();
        }
        List<PermissionTargetInfo> permissionTargetInfos = aclService
                .getPermissionTargets(ArtifactoryPermission.DEPLOY);
        Set<LocalRepoDescriptor> permittedDescriptors = Sets.newHashSet();
        Map<String, LocalRepoDescriptor> descriptorMap = centralConfigService.getDescriptor()
                .getLocalRepositoriesMap();
        for (PermissionTargetInfo permissionTargetInfo : permissionTargetInfos) {
            List<String> repoKeys = permissionTargetInfo.getRepoKeys();
            if (repoKeys.contains(PermissionTargetInfo.ANY_REPO)
                    || repoKeys.contains(PermissionTargetInfo.ANY_LOCAL_REPO)) {
                // return the list of all local repositories
                return getLocalRepoDescriptors();
            }
            for (String repoKey : repoKeys) {
                LocalRepoDescriptor permittedDescriptor = descriptorMap.get(repoKey);
                if (permittedDescriptor != null) {
                    permittedDescriptors.add(permittedDescriptor);
                }
            }
        }
        return Lists.newArrayList(permittedDescriptors);
    }

    @Override
    public boolean isRepoPathAccepted(RepoPath repoPath) {
        LocalRepo repo = getLocalOrCachedRepository(repoPath);
        return repo == null || repo.accepts(repoPath);
    }

    @Override
    public boolean isRepoPathVisible(RepoPath repoPath) {
        return (repoPath != null) && authService.canRead(repoPath)
                && (isRepoPathAccepted(repoPath) || authService.canAnnotate(repoPath));
    }

    @Override
    public boolean isRepoPathHandled(RepoPath repoPath) {
        String path = repoPath.getPath();
        if (repoPath.isRoot()) {
            return true;
        }
        LocalRepo repo = getLocalOrCachedRepository(repoPath);
        return repo.handlesReleaseSnapshot(path);
    }

    @Override
    public List<RemoteRepoDescriptor> getSharedRemoteRepoConfigs(String remoteUrl, Map<String, String> headersMap) {

        List<RemoteRepoDescriptor> remoteRepos = Lists.newArrayList();
        List<RepoDetails> remoteReposDetails = getSharedRemoteRepoDetails(remoteUrl, headersMap);
        boolean hasDefaultProxy = centralConfigService.defaultProxyDefined();
        ProxyDescriptor defaultProxy = centralConfigService.getMutableDescriptor().getDefaultProxy();
        for (RepoDetails remoteRepoDetails : remoteReposDetails) {
            String configurationUrl = remoteRepoDetails.getConfiguration();
            if (org.apache.commons.lang.StringUtils.isNotBlank(configurationUrl)) {
                RemoteRepoDescriptor remoteRepoConfig = getSharedRemoteRepoConfig(configurationUrl, headersMap);
                if (remoteRepoConfig != null) {
                    if (hasDefaultProxy && defaultProxy != null) {
                        ((HttpRepoDescriptor) remoteRepoConfig).setProxy(defaultProxy);
                    }
                    RepoLayout repoLayout = remoteRepoConfig.getRepoLayout();

                    //If there is no contained layout or if it doesn't exist locally, just add the default
                    if ((repoLayout == null)
                            || centralConfigService.getDescriptor().getRepoLayout(repoLayout.getName()) == null) {
                        remoteRepoConfig.setRepoLayout(RepoLayoutUtils.MAVEN_2_DEFAULT);
                    }

                    RepoLayout remoteRepoLayout = remoteRepoConfig.getRemoteRepoLayout();
                    //If there is contained layout doesn't exist locally, remove it
                    if ((remoteRepoLayout != null) && centralConfigService.getDescriptor()
                            .getRepoLayout(remoteRepoLayout.getName()) == null) {
                        remoteRepoConfig.setRemoteRepoLayout(null);
                    }

                    remoteRepos.add(remoteRepoConfig);
                }
            }
        }

        return remoteRepos;
    }

    @Override
    public Tree<ZipEntryInfo> zipEntriesToTree(RepoPath zipPath) throws IOException {
        LocalRepo localRepo = getLocalOrCachedRepository(zipPath);
        VfsFile file = localRepo.getImmutableFile(zipPath);
        ZipInputStream zin = null;
        try {
            Tree<ZipEntryInfo> tree;
            zin = new ZipInputStream(file.getStream());
            ZipEntry zipEntry;
            tree = InfoFactoryHolder.get().createZipEntriesTree();
            try {
                while ((zipEntry = zin.getNextEntry()) != null) {
                    tree.insert(InfoFactoryHolder.get().createZipEntry(zipEntry));
                }
                // IllegalArgumentException is being thrown from: java.util.zip.ZipInputStream.getUTF8String on a
                // bad archive
            } catch (IllegalArgumentException e) {
                throw new IOException(
                        "An error occurred while reading entries from zip file: " + file.getRepoPath());
            }
            return tree;
        } finally {
            IOUtils.closeQuietly(zin);
        }
    }

    @Override
    public ZipInputStream zipInputStream(RepoPath zipPath) throws IOException {
        LocalRepo localRepo = getLocalOrCachedRepository(zipPath);
        VfsFile file = localRepo.getImmutableFile(zipPath);
        return new ZipInputStream(file.getStream());
    }

    @Override
    public ArchiveInputStream archiveInputStream(RepoPath zipPath) throws IOException {
        LocalRepo localRepo = getLocalOrCachedRepository(zipPath);
        VfsFile file = localRepo.getImmutableFile(zipPath);
        String zipSuffix = zipPath.getPath().toLowerCase();
        return ZipUtils.returnArchiveInputStream(file.getStream(), zipSuffix);
    }

    @Override
    public ItemInfo getLastModified(RepoPath pathToSearch) {
        if (pathToSearch == null) {
            throw new IllegalArgumentException("Repo path cannot be null.");
        }
        if (!exists(pathToSearch)) {
            throw new ItemNotFoundRuntimeException("Could not find item: " + pathToSearch.getId());
        }

        return collectLastModified(pathToSearch);
    }

    private ItemInfo collectLastModified(RepoPath pathToSearch) {
        TreeBrowsingCriteria criteria = new TreeBrowsingCriteriaBuilder().applySecurity().build();
        ItemTree itemTree = new ItemTree(pathToSearch, criteria);
        LinkedList<ItemNode> fringe = Lists.newLinkedList();
        fringe.add(itemTree.getRootNode());
        ItemInfo lastModified = null;
        while (!fringe.isEmpty()) {
            ItemNode last = fringe.removeLast();
            if (last.hasChildren()) {
                fringe.addAll(last.getChildren());
            }
            if (!last.isFolder()) {
                if (lastModified == null || last.getItemInfo().getLastModified() > lastModified.getLastModified()) {
                    lastModified = last.getItemInfo();
                }
            }
        }
        return lastModified;
    }

    @Override
    public void touch(RepoPath repoPath) {
        if (repoPath == null) {
            throw new IllegalArgumentException("Repo path cannot be null.");
        }
        LocalRepo localOrCachedRepository = getLocalOrCachedRepository(repoPath);
        if (localOrCachedRepository == null) {
            throw new IllegalArgumentException(repoPath + " is not local or cache repository path");
        }
        MutableVfsItem mutableFsItem = localOrCachedRepository.getMutableFsItem(repoPath);
        if (mutableFsItem == null) {
            throw new ItemNotFoundRuntimeException("Could not find item: " + repoPath.getId());
        }
        mutableFsItem.setModified(System.currentTimeMillis());
    }

    @Override
    public void fixChecksums(RepoPath fileRepoPath) {
        MutableVfsFile mutableFile = getMutableFile(fileRepoPath);
        FileInfo fileInfo = mutableFile.getInfo();
        ChecksumsInfo checksumsInfo = fileInfo.getChecksumsInfo();
        for (ChecksumInfo checksumInfo : checksumsInfo.getChecksums()) {
            if (!checksumInfo.checksumsMatch()) {
                mutableFile.setClientChecksum(checksumInfo.getType(), ChecksumInfo.TRUSTED_FILE_MARKER);
            }
        }
    }

    private void exportAsync(@Nonnull String repoKey, ExportSettings settings) {
        MutableStatusHolder status = settings.getStatusHolder();
        TaskBase task = TaskUtils.createManualTask(ExportJob.class, 0L);
        task.addAttribute(Task.REPO_KEY, repoKey);
        task.addAttribute(ExportSettingsImpl.class.getName(), settings);
        taskService.startTask(task, true);
        boolean completed = taskService.waitForTaskCompletion(task.getToken());
        if (!completed) {
            if (!status.isError()) {
                // Add Error of no completion
                status.error("The task " + task + " did not complete correctly", log);
            }
        }
    }

    @Override
    public LocalRepo getLocalRepository(RepoPath repoPath) {
        String repoKey = repoPath.getRepoKey();
        LocalRepo localRepo = localOrCachedRepositoryByKey(repoKey);
        if (localRepo == null) {
            throw new IllegalArgumentException("Repository '" + repoKey + "' is not a local repository");
        }
        return localRepo;
    }

    private List<String> getLocalAndCacheRepoKeys() {
        List<String> result = new ArrayList<>();
        for (LocalRepoDescriptor localRepoDescriptor : getLocalAndCachedRepoDescriptors()) {
            result.add(localRepoDescriptor.getKey());
        }
        return result;
    }

    private void initAllRepoKeysCache() {
        Set<String> newKeys = new HashSet<>();
        newKeys.addAll(globalVirtualRepo.getLocalRepositoriesMap().keySet());
        newKeys.addAll(globalVirtualRepo.getRemoteRepositoriesMap().keySet());
        newKeys.addAll(globalVirtualRepo.getLocalCacheRepositoriesMap().keySet());
        newKeys.addAll(virtualRepositoriesMap.keySet());
        allRepoKeysCache = newKeys;
    }

    private RepoResource internalUnexpireIfExists(LocalRepo repo, String path) {
        // Need to release the read lock first
        RepoPath repoPath = InternalRepoPathFactory.create(repo.getKey(), path);
        RepoPath fsItemRepoPath = NamingUtils.getLockingTargetRepoPath(repoPath);
        // Write lock auto upgrade supported LockingHelper.releaseReadLock(fsItemRepoPath);
        MutableVfsItem fsItem = repo.getMutableFsItem(fsItemRepoPath);
        if (fsItem != null) {
            log.debug("{}: falling back to using cache entry for resource info at '{}'.", this, path);
            //Reset the resource age so it is kept being cached
            fsItem.setUpdated(System.currentTimeMillis());
            return repo.getInfo(new NullRequestContext(repoPath));
        }
        return null;
    }

    private void assertDelete(PathDeletionContext pathDeletionContext) {
        StoringRepo repo = pathDeletionContext.getRepo();
        String path = pathDeletionContext.getPath();
        BasicStatusHolder status = pathDeletionContext.getStatus();
        RepoPath repoPath = InternalRepoPathFactory.create(repo.getKey(), path);
        //Check that has delete rights to replace an exiting item
        if (repo.shouldProtectPathDeletion(pathDeletionContext)) {
            if (!authService.canDelete(repoPath)) {
                AccessLogger.deleteDenied(repoPath);
                if (centralConfigService.getDescriptor().getSecurity().isHideUnauthorizedResources()) {
                    status.error("Could not locate artifact '" + repoPath + "'.", HttpStatus.SC_NOT_FOUND, log);
                } else {
                    status.error(
                            "Not enough permissions to overwrite artifact '" + repoPath + "' (user '"
                                    + authService.currentUsername() + "' needs DELETE permission).",
                            HttpStatus.SC_FORBIDDEN, log);
                }
            }
        }

        //For deletion (as opposed to overwrite), check that path actually exists
        if (!pathDeletionContext.isAssertOverwrite() && !repo.itemExists(repoPath.getPath())) {
            status.error("Could not locate artifact '" + repoPath + "' (Nothing to delete).",
                    HttpStatus.SC_NOT_FOUND, log);
        }
    }

    // remove export folders of repositories that are not part of the current backup included repositories
    // this cleanup is needed in incremental backup when a repository is excluded from the backup or removed

    private void cleanupIncrementalBackupDirectory(File targetDir, List<String> reposToBackup) {
        if (!targetDir.exists()) {
            log.debug("Repositories backup directory doesn't exist: {}", targetDir.getAbsolutePath());
            return; // nothing to clean
        }
        File[] childFiles = targetDir.listFiles();
        for (File childFile : childFiles) {
            String fileName = childFile.getName();
            if (fileName.endsWith(METADATA_FOLDER)) {
                continue; // skip metadata folders, will delete them with the actual folder if needed
            }
            boolean includedInBackup = false;
            for (String repoKey : reposToBackup) {
                if (fileName.equals(repoKey)) {
                    includedInBackup = true;
                    break;
                }
            }
            if (!includedInBackup) {
                log.info("Deleting {} from the incremental backup dir since it is not part "
                        + "of the backup included repositories", childFile.getAbsolutePath());
                boolean deleted = FileUtils.deleteQuietly(childFile);
                if (!deleted) {
                    log.warn("Failed to delete {}", childFile.getAbsolutePath());
                }
                // now delete the metadata folder of the repository is it exists
                File metadataFolder = new File(childFile.getParentFile(), childFile.getName() + METADATA_FOLDER);
                if (metadataFolder.exists()) {
                    deleted = FileUtils.deleteQuietly(metadataFolder);
                    if (!deleted) {
                        log.warn("Failed to delete metadata folder {}", metadataFolder.getAbsolutePath());
                    }
                }
            }
        }
    }

    private LocalRepo getLocalOrCachedRepository(RepoPath repoPath) {
        return globalVirtualRepo.localOrCachedRepositoryByKey(repoPath.getRepoKey());
    }

    /**
     * check if repo exist already in case a new repo is created
     *
     * @param repoPath
     * @return true if exist in cache
     */
    @Override
    public boolean isRepoExistInCache(RepoPath repoPath) {
        return getLocalOrCachedRepository(repoPath) != null;
    }

    /**
     * Aggregates and unifies the given paths by parent
     *
     * @param pathsToMove        Paths to be moved\copied
     * @param targetLocalRepoKey Key of target local repo
     * @return Set of aggregated paths to move
     */
    private Set<RepoPath> aggregatePathsToMove(Set<RepoPath> pathsToMove, String targetLocalRepoKey, boolean copy) {
        // aggregate paths by parent repo path
        Multimap<RepoPath, RepoPath> pathsByParent = HashMultimap.create();
        for (RepoPath pathToMove : pathsToMove) {
            if (!pathToMove.getRepoKey().equals(targetLocalRepoKey)) {
                pathsByParent.put(pathToMove.getParent(), pathToMove);
            }
        }

        // now for each parent check if all its files are moved, and if they do, we will move
        // the parent folder and its children instead of just the children
        Set<RepoPath> pathsToMoveIncludingParents = new HashSet<>();
        for (RepoPath parentPath : pathsByParent.keySet()) {
            Collection<RepoPath> children = pathsByParent.get(parentPath);
            if (parentPath.isRoot()) {
                // parent is the repository itself and cannot be moved, just add the children
                pathsToMoveIncludingParents.addAll(children);
            } else {
                // if the parent children count equals to the number of files to be moved, move the folder instead
                LocalRepo repository = getLocalRepository(parentPath);
                VfsFolder folder = copy ? repository.getImmutableFolder(parentPath)
                        : repository.getMutableFolder(parentPath);
                // get all the folder children using write lock
                List<VfsItem> folderChildren = folder.getImmutableChildren();
                if (folder != null && folderChildren.size() == children.size()) {
                    pathsToMoveIncludingParents.add(parentPath);
                } else {
                    pathsToMoveIncludingParents.addAll(children);
                }
            }
        }
        return pathsToMoveIncludingParents;
    }

    private String adjustRefererValue(Map<String, String> headersMap, String headerVal) {
        //Append the artifactory uagent to the referer
        if (headerVal == null) {
            //Fallback to host
            headerVal = headersMap.get("HOST");
            if (headerVal == null) {
                //Fallback to unknown
                headerVal = "UNKNOWN";
            }
        }
        if (!headerVal.startsWith("http")) {
            headerVal = "http://" + headerVal;
        }
        try {
            URL uri = new URL(headerVal);
            //Only use the uri up to the path part
            headerVal = uri.getProtocol() + "://" + uri.getAuthority();
        } catch (MalformedURLException e) {
            //Nothing
        }
        headerVal += "/" + HttpUtils.getArtifactoryUserAgent();
        return headerVal;
    }

    /**
     * Returns a list of shared repository details
     *
     * @param remoteUrl  URL of remote Artifactory instance
     * @param headersMap Map of headers to set for client
     * @return List of shared repository details
     */
    private List<RepoDetails> getSharedRemoteRepoDetails(String remoteUrl, Map<String, String> headersMap) {
        StringBuilder urlBuilder = new StringBuilder();
        urlBuilder.append(remoteUrl);
        if (!remoteUrl.endsWith("/")) {
            urlBuilder.append("/");
        }
        urlBuilder.append(RestConstants.PATH_API).append("/").append(RepositoriesRestConstants.PATH_ROOT)
                .append("?").append(RepositoriesRestConstants.PARAM_REPO_TYPE).append("=")
                .append(RepoDetailsType.REMOTE.name());

        try (CloseableHttpResponse response = executeGetMethod(urlBuilder.toString(), headersMap)) {
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return JacksonReader.streamAsValueTypeReference(response.getEntity().getContent(),
                        new TypeReference<List<RepoDetails>>() {
                        });
            } else {
                return Lists.newArrayList();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns the shared remote repository descriptor from the given configuration URL
     *
     * @param configUrl  URL of repository configuration
     * @param headersMap Map of headers to set for client
     * @return RemoteRepoDescriptor
     */
    private RemoteRepoDescriptor getSharedRemoteRepoConfig(String configUrl, Map<String, String> headersMap) {
        try (CloseableHttpResponse response = executeGetMethod(configUrl, headersMap)) {
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return JacksonReader.streamAsValueTypeReference(response.getEntity().getContent(),
                        new TypeReference<HttpRepoDescriptor>() {
                        });
            } else {
                return null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Executes an HTTP GET method
     *
     * @param url        URL to query
     * @param headersMap Map of headers to set for client
     * @return The http response
     */
    private CloseableHttpResponse executeGetMethod(String url, Map<String, String> headersMap) throws IOException {
        HttpGet getMethod = new HttpGet(url);
        setHeader(getMethod, headersMap, HttpHeaders.USER_AGENT);
        setHeader(getMethod, headersMap, HttpHeaders.REFERER);

        ProxyDescriptor proxy = InternalContextHelper.get().getCentralConfig().getDescriptor().getDefaultProxy();

        CloseableHttpClient client = new HttpClientConfigurator().soTimeout(15000).connectionTimeout(15000)
                .retry(0, false).proxy(proxy).getClient();

        return client.execute(getMethod);
    }

    /**
     * Sets the HTTP headers for the given method
     *
     * @param getMethod  Get method that should be set with the headers
     * @param headersMap Map of headers to set
     * @param headerKey  Key of header to set
     */
    private void setHeader(HttpGet getMethod, Map<String, String> headersMap, String headerKey) {
        String headerVal = headersMap.get(headerKey.toUpperCase());
        if (HttpHeaders.REFERER.equalsIgnoreCase(headerKey)) {
            headerVal = adjustRefererValue(headersMap, headerVal);
        }
        if (headerVal != null) {
            getMethod.setHeader(headerKey, headerVal);
        }
    }

    private void registerRepositoriesMBeans() {
        MBeanRegistrationService registrationService = ContextHelper.get()
                .beanForType(MBeanRegistrationService.class);
        registrationService.unregisterAll(REPOSITORIES_MBEAN_TYPE);
        for (LocalRepoDescriptor descriptor : getLocalAndCachedRepoDescriptors()) {
            registrationService.register(new ManagedRepository(descriptor), REPOSITORIES_MBEAN_TYPE,
                    descriptor.getKey());
        }
    }
}