package org.apache.archiva.rest.services;
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.archiva.admin.model.RepositoryAdminException;
import org.apache.archiva.admin.model.beans.ManagedRepository;
import org.apache.archiva.common.utils.VersionComparator;
import org.apache.archiva.common.utils.VersionUtil;
import org.apache.archiva.dependency.tree.maven2.DependencyTreeBuilder;
import org.apache.archiva.maven2.metadata.MavenMetadataReader;
import org.apache.archiva.maven2.model.Artifact;
import org.apache.archiva.maven2.model.TreeEntry;
import org.apache.archiva.metadata.generic.GenericMetadataFacet;
import org.apache.archiva.metadata.model.ArtifactMetadata;
import org.apache.archiva.metadata.model.MetadataFacet;
import org.apache.archiva.metadata.model.ProjectVersionMetadata;
import org.apache.archiva.metadata.model.ProjectVersionReference;
import org.apache.archiva.metadata.repository.MetadataRepository;
import org.apache.archiva.metadata.repository.MetadataRepositoryException;
import org.apache.archiva.metadata.repository.MetadataResolutionException;
import org.apache.archiva.metadata.repository.MetadataResolver;
import org.apache.archiva.metadata.repository.RepositorySession;
import org.apache.archiva.metadata.repository.storage.maven2.ArtifactMetadataVersionComparator;
import org.apache.archiva.metadata.repository.storage.maven2.MavenProjectFacet;
import org.apache.archiva.model.ArchivaArtifact;
import org.apache.archiva.model.ArchivaRepositoryMetadata;
import org.apache.archiva.proxy.model.RepositoryProxyConnectors;
import org.apache.archiva.redback.components.cache.Cache;
import org.apache.archiva.repository.ManagedRepositoryContent;
import org.apache.archiva.repository.RepositoryContentFactory;
import org.apache.archiva.repository.RepositoryException;
import org.apache.archiva.repository.RepositoryNotFoundException;
import org.apache.archiva.repository.metadata.MetadataTools;
import org.apache.archiva.rest.api.model.ArtifactContent;
import org.apache.archiva.rest.api.model.ArtifactContentEntry;
import org.apache.archiva.rest.api.model.BrowseResult;
import org.apache.archiva.rest.api.model.BrowseResultEntry;
import org.apache.archiva.rest.api.model.Entry;
import org.apache.archiva.rest.api.model.MetadataAddRequest;
import org.apache.archiva.rest.api.model.VersionsList;
import org.apache.archiva.rest.api.services.ArchivaRestServiceException;
import org.apache.archiva.rest.api.services.BrowseService;
import org.apache.archiva.rest.services.utils.ArtifactContentEntryComparator;
import org.apache.archiva.security.ArchivaSecurityException;
import org.apache.archiva.xml.XMLException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
 * @author Olivier Lamy
 * @since 1.4-M3
 */
@Service( "browseService#rest" )
public class DefaultBrowseService
    extends AbstractRestService
    implements BrowseService
{

    @Inject
    private DependencyTreeBuilder dependencyTreeBuilder;

    @Inject
    private RepositoryContentFactory repositoryContentFactory;

    @Inject
    @Named( value = "repositoryProxyConnectors#default" )
    private RepositoryProxyConnectors connectors;

    @Inject
    @Named( value = "browse#versionMetadata" )
    private Cache<String, ProjectVersionMetadata> versionMetadataCache;

    @Override
    public BrowseResult getRootGroups( String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        Set<String> namespaces = new LinkedHashSet<String>();

        // TODO: this logic should be optional, particularly remembering we want to keep this code simple
        //       it is located here to avoid the content repository implementation needing to do too much for what
        //       is essentially presentation code
        Set<String> namespacesToCollapse = new LinkedHashSet<String>();
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            MetadataResolver metadataResolver = repositorySession.getResolver();

            for ( String repoId : selectedRepos )
            {
                namespacesToCollapse.addAll( metadataResolver.resolveRootNamespaces( repositorySession, repoId ) );
            }
            for ( String n : namespacesToCollapse )
            {
                // TODO: check performance of this
                namespaces.add( collapseNamespaces( repositorySession, metadataResolver, selectedRepos, n ) );
            }
        }
        catch ( MetadataResolutionException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            repositorySession.close();
        }

        List<BrowseResultEntry> browseGroupResultEntries = new ArrayList<>( namespaces.size() );
        for ( String namespace : namespaces )
        {
            browseGroupResultEntries.add( new BrowseResultEntry( namespace, false ) );
        }

        Collections.sort( browseGroupResultEntries );
        return new BrowseResult( browseGroupResultEntries );
    }

    @Override
    public BrowseResult browseGroupId( String groupId, String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        Set<String> projects = new LinkedHashSet<>();

        RepositorySession repositorySession = repositorySessionFactory.createSession();
        Set<String> namespaces;
        try
        {
            MetadataResolver metadataResolver = repositorySession.getResolver();

            Set<String> namespacesToCollapse = new LinkedHashSet<>();
            for ( String repoId : selectedRepos )
            {
                namespacesToCollapse.addAll( metadataResolver.resolveNamespaces( repositorySession, repoId, groupId ) );

                projects.addAll( metadataResolver.resolveProjects( repositorySession, repoId, groupId ) );
            }

            // TODO: this logic should be optional, particularly remembering we want to keep this code simple
            // it is located here to avoid the content repository implementation needing to do too much for what
            // is essentially presentation code
            namespaces = new LinkedHashSet<>();
            for ( String n : namespacesToCollapse )
            {
                // TODO: check performance of this
                namespaces.add(
                    collapseNamespaces( repositorySession, metadataResolver, selectedRepos, groupId + "." + n ) );
            }
        }
        catch ( MetadataResolutionException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            repositorySession.close();
        }
        List<BrowseResultEntry> browseGroupResultEntries = new ArrayList<>( namespaces.size() + projects.size() );
        for ( String namespace : namespaces )
        {
            browseGroupResultEntries.add( new BrowseResultEntry( namespace, false ).groupId( namespace ) );
        }
        for ( String project : projects )
        {
            browseGroupResultEntries.add(
                new BrowseResultEntry( groupId + '.' + project, true ).groupId( groupId ).artifactId( project ) );
        }
        Collections.sort( browseGroupResultEntries );
        return new BrowseResult( browseGroupResultEntries );

    }

    @Override
    public VersionsList getVersionsList( String groupId, String artifactId, String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        try
        {
            Collection<String> versions = getVersions( selectedRepos, groupId, artifactId );
            return new VersionsList( new ArrayList<>( versions ) );
        }
        catch ( MetadataResolutionException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }

    }

    private Collection<String> getVersions( List<String> selectedRepos, String groupId, String artifactId )
        throws MetadataResolutionException

    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            MetadataResolver metadataResolver = repositorySession.getResolver();

            Set<String> versions = new LinkedHashSet<String>();

            for ( String repoId : selectedRepos )
            {
                Collection<String> projectVersions =
                    metadataResolver.resolveProjectVersions( repositorySession, repoId, groupId, artifactId );
                versions.addAll( projectVersions );
            }

            List<String> sortedVersions = new ArrayList<>( versions );

            Collections.sort( sortedVersions, VersionComparator.getInstance() );

            return sortedVersions;
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public ProjectVersionMetadata getProjectMetadata( String groupId, String artifactId, String version,
                                                      String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        RepositorySession repositorySession = null;
        try
        {
            repositorySession = repositorySessionFactory.createSession();

            MetadataResolver metadataResolver = repositorySession.getResolver();

            ProjectVersionMetadata versionMetadata = null;
            for ( String repoId : selectedRepos )
            {
                if ( versionMetadata == null || versionMetadata.isIncomplete() )
                {
                    try
                    {
                        ProjectVersionMetadata versionMetadataTmp =
                            metadataResolver.resolveProjectVersion( repositorySession, repoId, groupId, artifactId,
                                                                    version );

                        if ( versionMetadata == null && versionMetadataTmp != null )
                        {
                            versionMetadata = versionMetadataTmp;
                        }


                    }
                    catch ( MetadataResolutionException e )
                    {
                        log.warn( "Skipping invalid metadata while compiling shared model for {}:{} in repo {}: {}",
                                  groupId, artifactId, repoId, e.getMessage() );
                    }
                }
            }

            return versionMetadata;
        }
        finally
        {
            if ( repositorySession != null )
            {
                repositorySession.close();
            }
        }

    }

    @Override
    public ProjectVersionMetadata getProjectVersionMetadata( String groupId, String artifactId, String repositoryId )
        throws ArchivaRestServiceException
    {

        List<String> selectedRepos = getSelectedRepos( repositoryId );

        RepositorySession repositorySession = null;
        try
        {

            Collection<String> projectVersions = getVersions( selectedRepos, groupId, artifactId );

            repositorySession = repositorySessionFactory.createSession();

            MetadataResolver metadataResolver = repositorySession.getResolver();

            ProjectVersionMetadata sharedModel = new ProjectVersionMetadata();

            MavenProjectFacet mavenFacet = new MavenProjectFacet();
            mavenFacet.setGroupId( groupId );
            mavenFacet.setArtifactId( artifactId );
            sharedModel.addFacet( mavenFacet );

            boolean isFirstVersion = true;

            for ( String version : projectVersions )
            {
                ProjectVersionMetadata versionMetadata = null;
                for ( String repoId : selectedRepos )
                {
                    if ( versionMetadata == null || versionMetadata.isIncomplete() )
                    {
                        try
                        {
                            ProjectVersionMetadata projectVersionMetadataResolved = null;
                            boolean useCache = !StringUtils.endsWith( version, VersionUtil.SNAPSHOT );
                            String cacheKey = null;
                            boolean cacheToUpdate = false;
                            // FIXME a bit maven centric!!!
                            // not a snapshot so get it from cache
                            if ( useCache )
                            {
                                cacheKey = repoId + groupId + artifactId + version;
                                projectVersionMetadataResolved = versionMetadataCache.get( cacheKey );
                            }
                            if ( useCache && projectVersionMetadataResolved != null )
                            {
                                versionMetadata = projectVersionMetadataResolved;
                            }
                            else
                            {
                                projectVersionMetadataResolved =
                                    metadataResolver.resolveProjectVersion( repositorySession, repoId, groupId,
                                                                            artifactId, version );
                                versionMetadata = projectVersionMetadataResolved;
                                cacheToUpdate = true;
                            }

                            if ( useCache && cacheToUpdate )
                            {
                                versionMetadataCache.put( cacheKey, projectVersionMetadataResolved );
                            }

                        }
                        catch ( MetadataResolutionException e )
                        {
                            log.error( "Skipping invalid metadata while compiling shared model for " + groupId + ":"
                                           + artifactId + " in repo " + repoId + ": " + e.getMessage() );
                        }
                    }
                }

                if ( versionMetadata == null )
                {
                    continue;
                }

                if ( isFirstVersion )
                {
                    sharedModel = versionMetadata;
                    sharedModel.setId( null );
                }
                else
                {
                    MavenProjectFacet versionMetadataMavenFacet =
                        (MavenProjectFacet) versionMetadata.getFacet( MavenProjectFacet.FACET_ID );
                    if ( versionMetadataMavenFacet != null )
                    {
                        if ( mavenFacet.getPackaging() != null //
                            && !StringUtils.equalsIgnoreCase( mavenFacet.getPackaging(),
                                                              versionMetadataMavenFacet.getPackaging() ) )
                        {
                            mavenFacet.setPackaging( null );
                        }
                    }

                    if ( StringUtils.isEmpty( sharedModel.getName() ) //
                        && !StringUtils.isEmpty( versionMetadata.getName() ) )
                    {
                        sharedModel.setName( versionMetadata.getName() );
                    }

                    if ( sharedModel.getDescription() != null //
                        && !StringUtils.equalsIgnoreCase( sharedModel.getDescription(),
                                                          versionMetadata.getDescription() ) )
                    {
                        sharedModel.setDescription( StringUtils.isNotEmpty( versionMetadata.getDescription() )
                                                        ? versionMetadata.getDescription()
                                                        : "" );
                    }

                    if ( sharedModel.getIssueManagement() != null //
                        && versionMetadata.getIssueManagement() != null //
                        && !StringUtils.equalsIgnoreCase( sharedModel.getIssueManagement().getUrl(),
                                                          versionMetadata.getIssueManagement().getUrl() ) )
                    {
                        sharedModel.setIssueManagement( versionMetadata.getIssueManagement() );
                    }

                    if ( sharedModel.getCiManagement() != null //
                        && versionMetadata.getCiManagement() != null //
                        && !StringUtils.equalsIgnoreCase( sharedModel.getCiManagement().getUrl(),
                                                          versionMetadata.getCiManagement().getUrl() ) )
                    {
                        sharedModel.setCiManagement( versionMetadata.getCiManagement() );
                    }

                    if ( sharedModel.getOrganization() != null //
                        && versionMetadata.getOrganization() != null //
                        && !StringUtils.equalsIgnoreCase( sharedModel.getOrganization().getName(),
                                                          versionMetadata.getOrganization().getName() ) )
                    {
                        sharedModel.setOrganization( versionMetadata.getOrganization() );
                    }

                    if ( sharedModel.getUrl() != null //
                        && !StringUtils.equalsIgnoreCase( sharedModel.getUrl(), versionMetadata.getUrl() ) )
                    {
                        sharedModel.setUrl( versionMetadata.getUrl() );
                    }
                }

                isFirstVersion = false;
            }
            return sharedModel;
        }
        catch ( MetadataResolutionException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            if ( repositorySession != null )
            {
                repositorySession.close();
            }
        }
    }

    @Override
    public List<TreeEntry> getTreeEntries( String groupId, String artifactId, String version, String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        try
        {
            return dependencyTreeBuilder.buildDependencyTree( selectedRepos, groupId, artifactId, version );
        }
        catch ( Exception e )
        {
            log.error( e.getMessage(), e );
        }

        return Collections.emptyList();
    }

    @Override
    public List<ManagedRepository> getUserRepositories()
        throws ArchivaRestServiceException
    {
        try
        {
            return userRepositories.getAccessibleRepositories( getPrincipal() );
        }
        catch ( ArchivaSecurityException e )
        {
            throw new ArchivaRestServiceException( "repositories.read.observable.error",
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
    }

    @Override
    public List<ManagedRepository> getUserManagableRepositories() throws ArchivaRestServiceException {
        try
        {
            return userRepositories.getManagableRepositories( getPrincipal() );
        }
        catch ( ArchivaSecurityException e )
        {
            throw new ArchivaRestServiceException( "repositories.read.managable.error",
                    Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
    }

    @Override
    public List<Artifact> getDependees( String groupId, String artifactId, String version, String repositoryId )
        throws ArchivaRestServiceException
    {
        List<ProjectVersionReference> references = new ArrayList<>();
        // TODO: what if we get duplicates across repositories?
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            MetadataResolver metadataResolver = repositorySession.getResolver();
            for ( String repoId : getObservableRepos() )
            {
                // TODO: what about if we want to see this irrespective of version?
                references.addAll(
                    metadataResolver.resolveProjectReferences( repositorySession, repoId, groupId, artifactId,
                                                               version ) );
            }
        }
        catch ( MetadataResolutionException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            repositorySession.close();
        }

        List<Artifact> artifacts = new ArrayList<>( references.size() );

        for ( ProjectVersionReference projectVersionReference : references )
        {
            artifacts.add( new Artifact( projectVersionReference.getNamespace(), projectVersionReference.getProjectId(),
                                         projectVersionReference.getProjectVersion() ) );
        }
        return artifacts;
    }

    @Override
    public List<Entry> getMetadatas( String groupId, String artifactId, String version, String repositoryId )
        throws ArchivaRestServiceException
    {
        ProjectVersionMetadata projectVersionMetadata =
            getProjectMetadata( groupId, artifactId, version, repositoryId );
        if ( projectVersionMetadata == null )
        {
            return Collections.emptyList();
        }
        MetadataFacet metadataFacet = projectVersionMetadata.getFacet( GenericMetadataFacet.FACET_ID );

        if ( metadataFacet == null )
        {
            return Collections.emptyList();
        }
        Map<String, String> map = metadataFacet.toProperties();
        List<Entry> entries = new ArrayList<>( map.size() );

        for ( Map.Entry<String, String> entry : map.entrySet() )
        {
            entries.add( new Entry( entry.getKey(), entry.getValue() ) );
        }

        return entries;
    }

    @Override
    public Boolean addMetadata( String groupId, String artifactId, String version, String key, String value,
                                String repositoryId )
        throws ArchivaRestServiceException
    {
        ProjectVersionMetadata projectVersionMetadata =
            getProjectMetadata( groupId, artifactId, version, repositoryId );

        if ( projectVersionMetadata == null )
        {
            return Boolean.FALSE;
        }

        Map<String, String> properties = new HashMap<>();

        MetadataFacet metadataFacet = projectVersionMetadata.getFacet( GenericMetadataFacet.FACET_ID );

        if ( metadataFacet != null && metadataFacet.toProperties() != null )
        {
            properties.putAll( metadataFacet.toProperties() );
        }
        else
        {
            metadataFacet = new GenericMetadataFacet();
        }

        properties.put( key, value );

        metadataFacet.fromProperties( properties );

        projectVersionMetadata.addFacet( metadataFacet );

        RepositorySession repositorySession = repositorySessionFactory.createSession();

        try
        {
            MetadataRepository metadataRepository = repositorySession.getRepository();

            metadataRepository.updateProjectVersion( repositoryId, groupId, artifactId, projectVersionMetadata );

            repositorySession.save();
        }
        catch ( MetadataRepositoryException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            repositorySession.close();
        }
        return Boolean.TRUE;
    }

    @Override
    public Boolean deleteMetadata( String groupId, String artifactId, String version, String key, String repositoryId )
        throws ArchivaRestServiceException
    {
        ProjectVersionMetadata projectVersionMetadata =
            getProjectMetadata( groupId, artifactId, version, repositoryId );

        if ( projectVersionMetadata == null )
        {
            return Boolean.FALSE;
        }

        GenericMetadataFacet metadataFacet =
            (GenericMetadataFacet) projectVersionMetadata.getFacet( GenericMetadataFacet.FACET_ID );

        if ( metadataFacet != null && metadataFacet.toProperties() != null )
        {
            Map<String, String> properties = metadataFacet.toProperties();
            properties.remove( key );
            metadataFacet.setAdditionalProperties( properties );
        }
        else
        {
            return Boolean.TRUE;
        }

        RepositorySession repositorySession = repositorySessionFactory.createSession();

        try
        {
            MetadataRepository metadataRepository = repositorySession.getRepository();

            metadataRepository.updateProjectVersion( repositoryId, groupId, artifactId, projectVersionMetadata );

            repositorySession.save();
        }
        catch ( MetadataRepositoryException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        finally
        {
            repositorySession.close();
        }
        return Boolean.TRUE;
    }

    @Override
    public List<ArtifactContentEntry> getArtifactContentEntries( String groupId, String artifactId, String version,
                                                                 String classifier, String type, String path,
                                                                 String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );
        try
        {
            for ( String repoId : selectedRepos )
            {

                ManagedRepositoryContent managedRepositoryContent =
                    repositoryContentFactory.getManagedRepositoryContent( repoId );
                ArchivaArtifact archivaArtifact = new ArchivaArtifact( groupId, artifactId, version, classifier,
                                                                       StringUtils.isEmpty( type ) ? "jar" : type,
                                                                       repoId );
                File file = managedRepositoryContent.toFile( archivaArtifact );
                if ( file.exists() )
                {
                    return readFileEntries( file, path, repoId );
                }
            }
        }
        catch ( IOException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        catch ( RepositoryNotFoundException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        catch ( RepositoryException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        return Collections.emptyList();
    }

    @Override
    public List<Artifact> getArtifactDownloadInfos( String groupId, String artifactId, String version,
                                                    String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        List<Artifact> artifactDownloadInfos = new ArrayList<>();

        try (RepositorySession session = repositorySessionFactory.createSession())
        {
            MetadataResolver metadataResolver = session.getResolver();
            for ( String repoId : selectedRepos )
            {
                List<ArtifactMetadata> artifacts = new ArrayList<>(
                    metadataResolver.resolveArtifacts( session, repoId, groupId, artifactId, version ) );
                Collections.sort( artifacts, ArtifactMetadataVersionComparator.INSTANCE );
                if ( artifacts != null && !artifacts.isEmpty() )
                {
                    return buildArtifacts( artifacts, repoId );
                }
            }
        }
        catch ( MetadataResolutionException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }

        return artifactDownloadInfos;
    }

    @Override
    public ArtifactContent getArtifactContentText( String groupId, String artifactId, String version, String classifier,
                                                   String type, String path, String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );
        try
        {
            for ( String repoId : selectedRepos )
            {

                ManagedRepositoryContent managedRepositoryContent =
                    repositoryContentFactory.getManagedRepositoryContent( repoId );
                ArchivaArtifact archivaArtifact = new ArchivaArtifact( groupId, artifactId, version, classifier,
                                                                       StringUtils.isEmpty( type ) ? "jar" : type,
                                                                       repoId );
                File file = managedRepositoryContent.toFile( archivaArtifact );
                if ( !file.exists() )
                {
                    log.debug( "file: {} not exists for repository: {} try next repository", file, repoId );
                    continue;
                }
                if ( StringUtils.isNotBlank( path ) )
                {
                    // zip entry of the path -> path must a real file entry of the archive
                    JarFile jarFile = new JarFile( file );
                    ZipEntry zipEntry = jarFile.getEntry( path );
                    try (InputStream inputStream = jarFile.getInputStream( zipEntry ))
                    {
                        return new ArtifactContent( IOUtils.toString( inputStream ), repoId );
                    }
                    finally
                    {
                        closeQuietly( jarFile );
                    }
                }
                return new ArtifactContent( FileUtils.readFileToString( file ), repoId );
            }
        }
        catch ( IOException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        catch ( RepositoryNotFoundException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        catch ( RepositoryException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        log.debug( "artifact: {}:{}:{}:{}:{} not found", groupId, artifactId, version, classifier, type );
        // 404 ?
        return new ArtifactContent();
    }

    @Override
    public Boolean artifactAvailable( String groupId, String artifactId, String version, String classifier,
                                      String repositoryId )
        throws ArchivaRestServiceException
    {
        List<String> selectedRepos = getSelectedRepos( repositoryId );

        boolean snapshot = VersionUtil.isSnapshot( version );

        try
        {
            for ( String repoId : selectedRepos )
            {

                ManagedRepository managedRepository = managedRepositoryAdmin.getManagedRepository( repoId );

                if ( ( snapshot && !managedRepository.isSnapshots() ) || ( !snapshot
                    && managedRepository.isSnapshots() ) )
                {
                    continue;
                }
                ManagedRepositoryContent managedRepositoryContent =
                    repositoryContentFactory.getManagedRepositoryContent( repoId );
                // FIXME default to jar which can be wrong for war zip etc....
                ArchivaArtifact archivaArtifact = new ArchivaArtifact( groupId, artifactId, version,
                                                                       StringUtils.isEmpty( classifier )
                                                                           ? ""
                                                                           : classifier, "jar", repoId );
                File file = managedRepositoryContent.toFile( archivaArtifact );

                if ( file != null && file.exists() )
                {
                    return true;
                }

                // in case of SNAPSHOT we can have timestamped version locally !
                if ( StringUtils.endsWith( version, VersionUtil.SNAPSHOT ) )
                {
                    File metadataFile = new File( file.getParent(), MetadataTools.MAVEN_METADATA );
                    if ( metadataFile.exists() )
                    {
                        try
                        {
                            ArchivaRepositoryMetadata archivaRepositoryMetadata =
                                MavenMetadataReader.read( metadataFile );
                            int buildNumber = archivaRepositoryMetadata.getSnapshotVersion().getBuildNumber();
                            String timeStamp = archivaRepositoryMetadata.getSnapshotVersion().getTimestamp();
                            // rebuild file name with timestamped version and build number
                            String timeStampFileName = new StringBuilder( artifactId ).append( '-' ) //
                                .append( StringUtils.remove( version, "-" + VersionUtil.SNAPSHOT ) ) //
                                .append( '-' ).append( timeStamp ) //
                                .append( '-' ).append( Integer.toString( buildNumber ) ) //
                                .append( ( StringUtils.isEmpty( classifier ) ? "" : "-" + classifier ) ) //
                                .append( ".jar" ).toString();

                            File timeStampFile = new File( file.getParent(), timeStampFileName );
                            log.debug( "try to find timestamped snapshot version file: {}", timeStampFile.getPath() );
                            if ( timeStampFile.exists() )
                            {
                                return true;
                            }
                        }
                        catch ( XMLException e )
                        {
                            log.warn( "skip fail to find timestamped snapshot file: {}", e.getMessage() );
                        }
                    }
                }

                String path = managedRepositoryContent.toPath( archivaArtifact );

                file = connectors.fetchFromProxies( managedRepositoryContent, path );

                if ( file != null && file.exists() )
                {
                    // download pom now
                    String pomPath = StringUtils.substringBeforeLast( path, ".jar" ) + ".pom";
                    connectors.fetchFromProxies( managedRepositoryContent, pomPath );
                    return true;
                }
            }
        }
        catch ( RepositoryAdminException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }
        catch ( RepositoryException e )
        {
            log.error( e.getMessage(), e );
            throw new ArchivaRestServiceException( e.getMessage(),
                                                   Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e );
        }

        return false;
    }

    @Override
    public Boolean artifactAvailable( String groupId, String artifactId, String version, String repositoryId )
        throws ArchivaRestServiceException
    {
        return artifactAvailable( groupId, artifactId, version, null, repositoryId );
    }

    @Override
    public List<Artifact> getArtifacts( String repositoryId )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas = repositorySession.getRepository().getArtifacts( repositoryId );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public List<Artifact> getArtifactsByProjectVersionMetadata( String key, String value, String repositoryId )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas = repositorySession.getRepository().getArtifactsByProjectVersionMetadata( key, value, repositoryId );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public List<Artifact> getArtifactsByMetadata( String key, String value, String repositoryId )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas = repositorySession.getRepository().getArtifactsByMetadata( key, value, repositoryId );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public List<Artifact> getArtifactsByProperty( String key, String value, String repositoryId )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas = repositorySession.getRepository().getArtifactsByProperty( key, value, repositoryId );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public Boolean importMetadata( MetadataAddRequest metadataAddRequest, String repositoryId )
        throws ArchivaRestServiceException
    {
        boolean result = true;
        for ( Map.Entry<String, String> metadata : metadataAddRequest.getMetadatas().entrySet() )
        {
            result = addMetadata( metadataAddRequest.getGroupId(), metadataAddRequest.getArtifactId(),
                                  metadataAddRequest.getVersion(), metadata.getKey(), metadata.getValue(),
                                  repositoryId );
            if ( !result )
            {
                break;
            }
        }
        return result;
    }

    @Override
    public List<Artifact> searchArtifacts( String text, String repositoryId, Boolean exact )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas =
                repositorySession.getRepository().searchArtifacts( text, repositoryId, exact == null ? false : exact );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    @Override
    public List<Artifact> searchArtifacts( String key, String text, String repositoryId, Boolean exact )
        throws ArchivaRestServiceException
    {
        RepositorySession repositorySession = repositorySessionFactory.createSession();
        try
        {
            List<ArtifactMetadata> artifactMetadatas =
                repositorySession.getRepository().searchArtifacts( key, text, repositoryId, exact == null ? false : exact );
            return buildArtifacts( artifactMetadatas, repositoryId );
        }
        catch ( MetadataRepositoryException e )
        {
            throw new ArchivaRestServiceException( e.getMessage(), e );
        }
        finally
        {
            repositorySession.close();
        }
    }

    //---------------------------
    // internals
    //---------------------------

    private void closeQuietly( JarFile jarFile )
    {
        if ( jarFile != null )
        {
            try
            {
                jarFile.close();
            }
            catch ( IOException e )
            {
                log.warn( "ignore error closing jarFile {}", jarFile.getName() );
            }
        }
    }

    protected List<ArtifactContentEntry> readFileEntries(final File file, final String filterPath, final String repoId )
        throws IOException
    {
        String cleanedfilterPath = filterPath==null ? "" : (StringUtils.startsWith(filterPath, "/") ?
                StringUtils.substringAfter(filterPath, "/") : filterPath);
        Map<String, ArtifactContentEntry> artifactContentEntryMap = new HashMap<>();
        int filterDepth = StringUtils.countMatches( cleanedfilterPath, "/" );
        if (!StringUtils.endsWith(cleanedfilterPath,"/") && !StringUtils.isEmpty(cleanedfilterPath)) {
            filterDepth++;
        }
        JarFile jarFile = new JarFile( file );
        try
        {
            Enumeration<JarEntry> jarEntryEnumeration = jarFile.entries();
            while ( jarEntryEnumeration.hasMoreElements() )
            {
                JarEntry currentEntry = jarEntryEnumeration.nextElement();
                String cleanedEntryName = StringUtils.endsWith( currentEntry.getName(), "/" ) ? //
                    StringUtils.substringBeforeLast( currentEntry.getName(), "/" ) : currentEntry.getName();
                String entryRootPath = getRootPath( cleanedEntryName );
                int depth = StringUtils.countMatches( cleanedEntryName, "/" );
                if ( StringUtils.isEmpty( cleanedfilterPath ) //
                    && !artifactContentEntryMap.containsKey( entryRootPath ) //
                    && depth == filterDepth )
                {

                    artifactContentEntryMap.put( entryRootPath,
                                                 new ArtifactContentEntry( entryRootPath, !currentEntry.isDirectory(),
                                                                           depth, repoId ) );
                }
                else
                {
                    if ( StringUtils.startsWith( cleanedEntryName, cleanedfilterPath ) //
                        && ( depth == filterDepth || ( !currentEntry.isDirectory() && depth == filterDepth ) ) )
                    {
                        artifactContentEntryMap.put( cleanedEntryName, new ArtifactContentEntry( cleanedEntryName,
                                                                                                 !currentEntry.isDirectory(),
                                                                                                 depth, repoId ) );
                    }
                }
            }

            if ( StringUtils.isNotEmpty( cleanedfilterPath ) )
            {
                Map<String, ArtifactContentEntry> filteredArtifactContentEntryMap = new HashMap<>();

                for ( Map.Entry<String, ArtifactContentEntry> entry : artifactContentEntryMap.entrySet() )
                {
                    filteredArtifactContentEntryMap.put( entry.getKey(), entry.getValue() );
                }

                List<ArtifactContentEntry> sorted = getSmallerDepthEntries( filteredArtifactContentEntryMap );
                if ( sorted == null )
                {
                    return Collections.emptyList();
                }
                Collections.sort( sorted, ArtifactContentEntryComparator.INSTANCE );
                return sorted;
            }
        }
        finally
        {
            if ( jarFile != null )
            {
                jarFile.close();
            }
        }
        List<ArtifactContentEntry> sorted = new ArrayList<>( artifactContentEntryMap.values() );
        Collections.sort( sorted, ArtifactContentEntryComparator.INSTANCE );
        return sorted;
    }

    private List<ArtifactContentEntry> getSmallerDepthEntries( Map<String, ArtifactContentEntry> entries )
    {
        int smallestDepth = Integer.MAX_VALUE;
        Map<Integer, List<ArtifactContentEntry>> perDepthList = new HashMap<>();
        for ( Map.Entry<String, ArtifactContentEntry> entry : entries.entrySet() )
        {

            ArtifactContentEntry current = entry.getValue();

            if ( current.getDepth() < smallestDepth )
            {
                smallestDepth = current.getDepth();
            }

            List<ArtifactContentEntry> currentList = perDepthList.get( current.getDepth() );

            if ( currentList == null )
            {
                currentList = new ArrayList<>();
                currentList.add( current );
                perDepthList.put( current.getDepth(), currentList );
            }
            else
            {
                currentList.add( current );
            }

        }

        return perDepthList.get( smallestDepth );
    }

    /**
     * @param path
     * @return org/apache -&gt; org , org -&gt; org
     */
    private String getRootPath( String path )
    {
        if ( StringUtils.contains( path, '/' ) )
        {
            return StringUtils.substringBefore( path, "/" );
        }
        return path;
    }

    private List<String> getSelectedRepos( String repositoryId )
        throws ArchivaRestServiceException
    {

        List<String> selectedRepos = getObservableRepos();

        if ( CollectionUtils.isEmpty( selectedRepos ) )
        {
            return Collections.emptyList();
        }

        if ( StringUtils.isNotEmpty( repositoryId ) )
        {
            // check user has karma on the repository
            if ( !selectedRepos.contains( repositoryId ) )
            {
                throw new ArchivaRestServiceException( "browse.root.groups.repositoy.denied",
                                                       Response.Status.FORBIDDEN.getStatusCode(), null );
            }
            selectedRepos = Collections.singletonList( repositoryId );
        }
        return selectedRepos;
    }


    private String collapseNamespaces( RepositorySession repositorySession, MetadataResolver metadataResolver,
                                       Collection<String> repoIds, String n )
        throws MetadataResolutionException
    {
        Set<String> subNamespaces = new LinkedHashSet<String>();
        for ( String repoId : repoIds )
        {
            subNamespaces.addAll( metadataResolver.resolveNamespaces( repositorySession, repoId, n ) );
        }
        if ( subNamespaces.size() != 1 )
        {
            log.debug( "{} is not collapsible as it has sub-namespaces: {}", n, subNamespaces );
            return n;
        }
        else
        {
            for ( String repoId : repoIds )
            {
                Collection<String> projects = metadataResolver.resolveProjects( repositorySession, repoId, n );
                if ( projects != null && !projects.isEmpty() )
                {
                    log.debug( "{} is not collapsible as it has projects", n );
                    return n;
                }
            }
            return collapseNamespaces( repositorySession, metadataResolver, repoIds,
                                       n + "." + subNamespaces.iterator().next() );
        }
    }

    public Cache getVersionMetadataCache()
    {
        return versionMetadataCache;
    }

    public void setVersionMetadataCache( Cache versionMetadataCache )
    {
        this.versionMetadataCache = versionMetadataCache;
    }
}
