/*
 *  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
 *
 *      https://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.
 *
 */
package org.apache.ivy.plugins.resolver;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.IvyPatternHelper;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.cache.ArtifactOrigin;
import org.apache.ivy.core.cache.ModuleDescriptorWriter;
import org.apache.ivy.core.cache.RepositoryCacheManager;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.ArtifactDownloadReport;
import org.apache.ivy.core.report.DownloadReport;
import org.apache.ivy.core.report.DownloadStatus;
import org.apache.ivy.core.report.MetadataArtifactDownloadReport;
import org.apache.ivy.core.resolve.DownloadOptions;
import org.apache.ivy.core.resolve.IvyNode;
import org.apache.ivy.core.resolve.ResolveData;
import org.apache.ivy.core.resolve.ResolvedModuleRevision;
import org.apache.ivy.core.search.ModuleEntry;
import org.apache.ivy.core.search.OrganisationEntry;
import org.apache.ivy.core.search.RevisionEntry;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.latest.ArtifactInfo;
import org.apache.ivy.plugins.namespace.Namespace;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ModuleDescriptorParserRegistry;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
import org.apache.ivy.plugins.repository.ArtifactResourceResolver;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.repository.ResourceDownloader;
import org.apache.ivy.plugins.repository.file.FileRepository;
import org.apache.ivy.plugins.repository.file.FileResource;
import org.apache.ivy.plugins.repository.url.URLRepository;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.plugins.resolver.util.MDResolvedResource;
import org.apache.ivy.plugins.resolver.util.ResolvedResource;
import org.apache.ivy.plugins.resolver.util.ResourceMDParser;
import org.apache.ivy.plugins.version.VersionMatcher;
import org.apache.ivy.util.Checks;
import org.apache.ivy.util.ChecksumHelper;
import org.apache.ivy.util.DateUtil;
import org.apache.ivy.util.HostUtil;
import org.apache.ivy.util.Message;

import static org.apache.ivy.util.StringUtils.splitToArray;

/**
 *
 */
public abstract class BasicResolver extends AbstractResolver {
    public static final String DESCRIPTOR_OPTIONAL = "optional";

    public static final String DESCRIPTOR_REQUIRED = "required";

    /**
     * Exception thrown internally in getDependency to indicate a dependency is unresolved.
     * <p>
     * Due to the contract of getDependency, this exception is never thrown publicly, but rather
     * converted in a message (either error or verbose) and returning null
     * </p>
     */
    private static class UnresolvedDependencyException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        private boolean error;

        /**
         * Dependency has not been resolved. This is not an error and won't log any message.
         */
        public UnresolvedDependencyException() {
            this("", false);
        }

        /**
         * Dependency has not been resolved. This is an error and will log a message.
         */
        public UnresolvedDependencyException(String message) {
            this(message, true);
        }

        /**
         * Dependency has not been resolved. The boolean tells if it is an error or not, a message
         * will be logged if non empty.
         */
        public UnresolvedDependencyException(String message, boolean error) {
            super(message);
            this.error = error;
        }

        public boolean isError() {
            return error;
        }
    }

    @Deprecated
    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(
            DateUtil.DATE_FORMAT_PATTERN);

    private String workspaceName;

    /**
     * True if the files resolved are dependent of the environment from which they have been
     * resolved, false otherwise. In general, relative paths are dependent of the environment, and
     * absolute paths including machine reference are not.
     */
    private boolean envDependent = true;

    private List<String> ivyattempts = new ArrayList<>();

    private Map<Artifact, List<String>> artattempts = new HashMap<>();

    private boolean checkconsistency = true;

    private boolean allownomd = true;

    private boolean force = false;

    private String checksums = null;

    private URLRepository extartifactrep = new URLRepository(); // used only to download

    // external artifacts

    public BasicResolver() {
        workspaceName = HostUtil.getLocalHostName();
    }

    public String getWorkspaceName() {
        return workspaceName;
    }

    public void setWorkspaceName(String workspaceName) {
        this.workspaceName = workspaceName;
    }

    public boolean isEnvDependent() {
        return envDependent;
    }

    public void setEnvDependent(boolean envDependent) {
        this.envDependent = envDependent;
    }

    public ResolvedModuleRevision getDependency(DependencyDescriptor dd, ResolveData data)
            throws ParseException {
        IvyContext context = IvyContext.pushNewCopyContext();
        try {
            ResolvedModuleRevision mr = data.getCurrentResolvedModuleRevision();
            if (mr != null && shouldReturnResolvedModule(dd, mr)) {
                return mr;
            }

            if (isForce()) {
                dd = dd.clone(ModuleRevisionId.newInstance(dd.getDependencyRevisionId(),
                    "latest.integration"));
            }
            DependencyDescriptor systemDd = dd;
            DependencyDescriptor nsDd = fromSystem(dd);
            context.setDependencyDescriptor(systemDd);
            context.setResolveData(data);

            clearIvyAttempts();
            clearArtifactAttempts();
            ModuleRevisionId systemMrid = systemDd.getDependencyRevisionId();
            ModuleRevisionId nsMrid = nsDd.getDependencyRevisionId();

            checkRevision(systemMrid);

            boolean isDynamic = getAndCheckIsDynamic(systemMrid);

            // we first search for the dependency in cache
            ResolvedModuleRevision rmr = findModuleInCache(systemDd, data);
            if (rmr != null) {
                if (rmr.getDescriptor().isDefault() && rmr.getResolver() != this) {
                    Message.verbose("\t" + getName() + ": found revision in cache: " + systemMrid
                            + " (resolved by " + rmr.getResolver().getName()
                            + "): but it's a default one, maybe we can find a better one");
                } else if (isForce() && rmr.getResolver() != this) {
                    Message.verbose("\t" + getName() + ": found revision in cache: " + systemMrid
                            + " (resolved by " + rmr.getResolver().getName()
                            + "): but we are in force mode, let's try to find one ourselves");
                } else {
                    Message.verbose("\t" + getName() + ": revision in cache: " + systemMrid);
                    return checkLatest(systemDd, checkForcedResolvedModuleRevision(rmr), data);
                }
            }
            if (data.getOptions().isUseCacheOnly()) {
                throw new UnresolvedDependencyException("\t" + getName()
                        + " (useCacheOnly) : no ivy file found for " + systemMrid, false);
            }

            checkInterrupted();

            ResolvedResource ivyRef = findIvyFileRef(nsDd, data);
            checkInterrupted();

            // get module descriptor
            ModuleDescriptor nsMd;
            ModuleDescriptor systemMd = null;
            if (ivyRef == null) {
                if (!isAllownomd()) {
                    throw new UnresolvedDependencyException("\t" + getName()
                            + ": no ivy file found for " + systemMrid, false);
                }

                nsMd = DefaultModuleDescriptor.newDefaultInstance(nsMrid,
                    nsDd.getAllDependencyArtifacts());

                ModuleDescriptor md = dd.getModuleDescriptor();
                if (md != null) {
                    Map<String, String> extraAttributesNamespaces = md.getExtraAttributesNamespaces();
                    for (Map.Entry<String, String> entry : extraAttributesNamespaces.entrySet()) {
                      ((DefaultModuleDescriptor) nsMd).addExtraAttributeNamespace(entry.getKey(), entry.getValue());
                    }
                }

                ResolvedResource artifactRef = findFirstArtifactRef(nsMd, nsDd, data);
                checkInterrupted();

                if (artifactRef == null) {
                    throw new UnresolvedDependencyException("\t" + getName()
                            + ": no ivy file nor artifact found for " + systemMrid, false);
                }

                long lastModified = artifactRef.getLastModified();
                if (lastModified != 0 && nsMd instanceof DefaultModuleDescriptor) {
                    ((DefaultModuleDescriptor) nsMd).setLastModified(lastModified);
                }
                Message.verbose("\t" + getName() + ": no ivy file found for " + systemMrid
                        + ": using default data");
                if (isDynamic) {
                    nsMd.setResolvedModuleRevisionId(ModuleRevisionId.newInstance(nsMrid,
                        artifactRef.getRevision()));
                }
                systemMd = toSystem(nsMd);
                MetadataArtifactDownloadReport madr = new MetadataArtifactDownloadReport(
                        systemMd.getMetadataArtifact());
                madr.setDownloadStatus(DownloadStatus.NO);
                madr.setSearched(true);
                rmr = new ResolvedModuleRevision(this, this, systemMd, madr, isForce());
                getRepositoryCacheManager().cacheModuleDescriptor(this, artifactRef,
                    toSystem(dd), systemMd.getAllArtifacts()[0], null, getCacheOptions(data));
            } else {
                if (ivyRef instanceof MDResolvedResource) {
                    rmr = ((MDResolvedResource) ivyRef).getResolvedModuleRevision();
                }
                if (rmr == null) {
                    rmr = parse(ivyRef, systemDd, data);
                    if (rmr == null) {
                        throw new UnresolvedDependencyException();
                    }
                }
                if (!rmr.getReport().isDownloaded() && rmr.getReport().getLocalFile() != null) {
                    return checkLatest(systemDd, checkForcedResolvedModuleRevision(rmr), data);
                }
                nsMd = rmr.getDescriptor();

                // check descriptor data is in sync with resource revision and names
                systemMd = toSystem(nsMd);
                if (isCheckconsistency()) {
                    checkDescriptorConsistency(systemMrid, systemMd, ivyRef);
                    checkDescriptorConsistency(nsMrid, nsMd, ivyRef);
                } else {
                    if (systemMd instanceof DefaultModuleDescriptor) {
                        DefaultModuleDescriptor defaultMd = (DefaultModuleDescriptor) systemMd;
                        ModuleRevisionId revision = getRevision(ivyRef, systemMrid, systemMd);
                        defaultMd.setModuleRevisionId(revision);
                        defaultMd.setResolvedModuleRevisionId(revision);
                    } else {
                        Message.warn(
                            "consistency disabled with instance of non DefaultModuleDescriptor... module info can't be updated, so consistency check will be done");
                        checkDescriptorConsistency(nsMrid, nsMd, ivyRef);
                        checkDescriptorConsistency(systemMrid, systemMd, ivyRef);
                    }
                }
                rmr = new ResolvedModuleRevision(this, this, systemMd,
                        toSystem(rmr.getReport()), isForce());
            }

            resolveAndCheckRevision(systemMd, systemMrid, ivyRef, isDynamic);
            resolveAndCheckPublicationDate(systemDd, systemMd, systemMrid, data);
            checkNotConvertedExclusionRule(systemMd, ivyRef, data);

            if (ivyRef == null || ivyRef.getResource() != null) {
                cacheModuleDescriptor(systemMd, systemMrid, ivyRef, rmr);
            }

            return checkLatest(systemDd, checkForcedResolvedModuleRevision(rmr), data);
        } catch (UnresolvedDependencyException ex) {
            if (!ex.getMessage().isEmpty()) {
                if (ex.isError()) {
                    Message.error(ex.getMessage());
                } else {
                    Message.verbose(ex.getMessage());
                }
            }
            return data.getCurrentResolvedModuleRevision();
        } finally {
            IvyContext.popContext();
        }
    }

    protected boolean shouldReturnResolvedModule(DependencyDescriptor dd, ResolvedModuleRevision mr) {
        // a resolved module revision has already been found by a prior dependency resolver
        // let's see if it should be returned and bypass this resolver

        ModuleRevisionId mrid = dd.getDependencyRevisionId();
        boolean isDynamic = getSettings().getVersionMatcher().isDynamic(mrid);
        boolean shouldReturn = mr.isForce();
        shouldReturn |= !isDynamic && !mr.getDescriptor().isDefault();
        shouldReturn &= !isForce();

        return shouldReturn;
    }

    private ResolvedModuleRevision checkForcedResolvedModuleRevision(ResolvedModuleRevision rmr) {
        if (rmr == null) {
            return null;
        }
        if (!isForce() || rmr.isForce()) {
            return rmr;
        }
        return new ResolvedModuleRevision(rmr.getResolver(), rmr.getArtifactResolver(),
                rmr.getDescriptor(), rmr.getReport(), true);
    }

    private void cacheModuleDescriptor(ModuleDescriptor systemMd, ModuleRevisionId systemMrid,
            ResolvedResource ivyRef, ResolvedModuleRevision rmr) {
        RepositoryCacheManager cacheManager = getRepositoryCacheManager();

        final ModuleDescriptorParser parser = systemMd.getParser();

        // the metadata artifact which was used to cache the original metadata file
        Artifact requestedMetadataArtifact = (ivyRef == null) ? systemMd.getMetadataArtifact()
                : parser.getMetadataArtifact(
                        ModuleRevisionId.newInstance(systemMrid, systemMd.getRevision()),
                ivyRef.getResource());

        cacheManager.originalToCachedModuleDescriptor(this, ivyRef, requestedMetadataArtifact, rmr,
            new ModuleDescriptorWriter() {
                public void write(ResolvedResource originalMdResource, ModuleDescriptor md,
                        File src, File dest) throws IOException, ParseException {
                    if (originalMdResource == null) {
                        // a basic ivy file is written containing default data
                        XmlModuleDescriptorWriter.write(md, dest);
                    } else {
                        // copy and update ivy file from source to cache
                        parser.toIvyFile(new FileInputStream(src),
                            originalMdResource.getResource(), dest, md);
                        long repLastModified = originalMdResource.getLastModified();
                        if (repLastModified > 0) {
                            dest.setLastModified(repLastModified);
                        }
                    }
                }
            });
    }

    private void checkNotConvertedExclusionRule(ModuleDescriptor systemMd, ResolvedResource ivyRef,
            ResolveData data) {
        if (!getNamespace().equals(Namespace.SYSTEM_NAMESPACE) && !systemMd.isDefault()
                && data.getSettings().logNotConvertedExclusionRule()
                && systemMd instanceof DefaultModuleDescriptor) {
            DefaultModuleDescriptor dmd = (DefaultModuleDescriptor) systemMd;
            if (dmd.isNamespaceUseful()) {
                Message.warn("the module descriptor " + ivyRef.getResource()
                        + " has information which can't be converted into the system namespace. It will require the availability of the namespace '"
                        + getNamespace().getName() + "' to be fully usable.");
            }
        }
    }

    private void resolveAndCheckPublicationDate(DependencyDescriptor systemDd,
            ModuleDescriptor systemMd, ModuleRevisionId systemMrid, ResolveData data) {
        if (data.getDate() == null) {
            return;
        }
        // resolve and check publication date
        long pubDate = getPublicationDate(systemMd, systemDd, data);
        if (pubDate > data.getDate().getTime()) {
            throw new UnresolvedDependencyException("\t" + getName()
                    + ": unacceptable publication date => was=" + new Date(pubDate)
                    + " required=" + data.getDate());
        }
        if (pubDate == -1) {
            throw new UnresolvedDependencyException("\t" + getName()
                    + ": impossible to guess publication date: artifact missing for "
                    + systemMrid);
        }
        systemMd.setResolvedPublicationDate(new Date(pubDate));
    }

    protected void checkModuleDescriptorRevision(ModuleDescriptor systemMd,
            ModuleRevisionId systemMrid) {
        if (!getSettings().getVersionMatcher().accept(systemMrid, systemMd)) {
            throw new UnresolvedDependencyException("\t" + getName()
                    + ": unacceptable revision => was="
                    + systemMd.getResolvedModuleRevisionId().getRevision() + " required="
                    + systemMrid.getRevision());
        }
    }

    private boolean getAndCheckIsDynamic(ModuleRevisionId systemMrid) {
        boolean isDynamic = getSettings().getVersionMatcher().isDynamic(systemMrid);
        if (isDynamic && !acceptLatest()) {
            throw new UnresolvedDependencyException("dynamic revisions not handled by "
                    + getClass().getName() + ". impossible to resolve " + systemMrid);
        }
        return isDynamic;
    }

    private void checkRevision(ModuleRevisionId systemMrid) {
        // check revision
        int index = systemMrid.getRevision().indexOf('@');
        if (index != -1 && !systemMrid.getRevision().substring(index + 1).equals(workspaceName)) {
            throw new UnresolvedDependencyException("\t" + getName() + ": unhandled revision => "
                    + systemMrid.getRevision());
        }
    }

    private void resolveAndCheckRevision(ModuleDescriptor systemMd,
            ModuleRevisionId dependencyConstraint, ResolvedResource ivyRef, boolean isDynamic) {
        // we get the resolved module revision id from the descriptor: it may contain extra
        // attributes that were not included in the dependency constraint
        ModuleRevisionId resolvedMrid = systemMd.getResolvedModuleRevisionId();
        if (resolvedMrid.getRevision() == null || resolvedMrid.getRevision().length() == 0
                || resolvedMrid.getRevision().startsWith("working@")) {
            if (!isDynamic) {
                resolvedMrid = ModuleRevisionId.newInstance(resolvedMrid,
                    dependencyConstraint.getRevision());
            } else if (ivyRef == null) {
                resolvedMrid = systemMd.getMetadataArtifact().getModuleRevisionId();
            } else if (ivyRef.getRevision() == null || ivyRef.getRevision().length() == 0) {
                resolvedMrid = ModuleRevisionId.newInstance(resolvedMrid, "working@" + getName());
            } else {
                resolvedMrid = ModuleRevisionId.newInstance(resolvedMrid, ivyRef.getRevision());
            }
        }
        if (isDynamic) {
            Message.verbose("\t\t[" + toSystem(resolvedMrid).getRevision() + "] "
                    + dependencyConstraint.getModuleId());
        }
        systemMd.setResolvedModuleRevisionId(resolvedMrid);
        checkModuleDescriptorRevision(systemMd, dependencyConstraint);
    }

    private ModuleRevisionId getRevision(ResolvedResource ivyRef, ModuleRevisionId askedMrid,
            ModuleDescriptor md) {
        Map<String, String> allAttributes = new HashMap<>();
        allAttributes.putAll(md.getQualifiedExtraAttributes());
        allAttributes.putAll(askedMrid.getQualifiedExtraAttributes());

        String revision = ivyRef.getRevision();
        if (revision == null) {
            Message.debug("no revision found in reference for " + askedMrid);
            if (getSettings().getVersionMatcher().isDynamic(askedMrid)) {
                if (md.getModuleRevisionId().getRevision() == null) {
                    revision = "working@" + getName();
                } else {
                    Message.debug("using " + askedMrid);
                    revision = askedMrid.getRevision();
                }
            } else {
                Message.debug("using " + askedMrid);
                revision = askedMrid.getRevision();
            }
        }

        return ModuleRevisionId.newInstance(askedMrid.getOrganisation(), askedMrid.getName(),
            askedMrid.getBranch(), revision, allAttributes);
    }

    public ResolvedModuleRevision parse(final ResolvedResource mdRef, DependencyDescriptor dd,
            ResolveData data) throws ParseException {

        DependencyDescriptor nsDd = dd;
        dd = toSystem(nsDd);

        ModuleRevisionId mrid = dd.getDependencyRevisionId();
        ModuleDescriptorParser parser = ModuleDescriptorParserRegistry.getInstance().getParser(
            mdRef.getResource());
        if (parser == null) {
            Message.warn("no module descriptor parser available for " + mdRef.getResource());
            return null;
        }
        Message.verbose("\t" + getName() + ": found md file for " + mrid);
        Message.verbose("\t\t=> " + mdRef);
        Message.debug("\tparser = " + parser);

        ModuleRevisionId resolvedMrid = mrid;

        // first check if this dependency has not yet been resolved
        if (getSettings().getVersionMatcher().isDynamic(mrid)) {
            resolvedMrid = ModuleRevisionId.newInstance(mrid, mdRef.getRevision());
            IvyNode node = data.getNode(resolvedMrid);
            if (node != null && node.getModuleRevision() != null) {
                // this revision has already be resolved : return it
                if (node.getDescriptor() == null || !node.getDescriptor().isDefault()) {
                    Message.verbose("\t" + getName() + ": revision already resolved: "
                            + resolvedMrid);
                    node.getModuleRevision().getReport().setSearched(true);
                    return node.getModuleRevision();
                }
                Message.verbose("\t" + getName() + ": found already resolved revision: "
                        + resolvedMrid
                        + ": but it's a default one, maybe we can find a better one");
            }
        }

        Artifact moduleArtifact = parser.getMetadataArtifact(resolvedMrid, mdRef.getResource());
        return getRepositoryCacheManager().cacheModuleDescriptor(this, mdRef, dd, moduleArtifact,
            downloader, getCacheOptions(data));
    }

    protected ResourceMDParser getRMDParser(final DependencyDescriptor dd, final ResolveData data) {
        return new ResourceMDParser() {
            public MDResolvedResource parse(Resource resource, String rev) {
                try {
                    ResolvedModuleRevision rmr = BasicResolver.this.parse(new ResolvedResource(
                            resource, rev), dd, data);
                    if (rmr != null) {
                        return new MDResolvedResource(resource, rev, rmr);
                    }
                } catch (ParseException e) {
                    Message.warn("Failed to parse the file '" + resource + "'", e);
                }
                return null;
            }

        };
    }

    protected ResourceMDParser getDefaultRMDParser(final ModuleId mid) {
        return new ResourceMDParser() {
            public MDResolvedResource parse(Resource resource, String rev) {
                DefaultModuleDescriptor md = DefaultModuleDescriptor
                        .newDefaultInstance(new ModuleRevisionId(mid, rev));
                MetadataArtifactDownloadReport madr = new MetadataArtifactDownloadReport(
                        md.getMetadataArtifact());
                madr.setDownloadStatus(DownloadStatus.NO);
                madr.setSearched(true);
                return new MDResolvedResource(resource, rev, new ResolvedModuleRevision(
                        BasicResolver.this, BasicResolver.this, md, madr, isForce()));
            }
        };
    }

    // private boolean isResolved(ResolveData data, ModuleRevisionId mrid) {
    // IvyNode node = getSystemNode(data, mrid);
    // return node != null && node.getModuleRevision() != null;
    // }
    //
    private void checkDescriptorConsistency(ModuleRevisionId mrid, ModuleDescriptor md,
            ResolvedResource ivyRef) throws ParseException {
        boolean ok = true;
        StringBuilder errors = new StringBuilder();
        if (!mrid.getOrganisation().equals(md.getModuleRevisionId().getOrganisation())) {
            Message.error(String.format("\t%s: bad organisation found in %s: expected='%s' found='%s'",
                    getName(), ivyRef.getResource(), mrid.getOrganisation(),
                    md.getModuleRevisionId().getOrganisation()));
            errors.append("bad organisation: expected='").append(mrid.getOrganisation())
                    .append("' found='").append(md.getModuleRevisionId().getOrganisation()).append("'; ");
            ok = false;
        }
        if (!mrid.getName().equals(md.getModuleRevisionId().getName())) {
            Message.error(String.format("\t%s: bad module name found in %s: expected='%s found='%s'",
                    getName(), ivyRef.getResource(), mrid.getName(),
                    md.getModuleRevisionId().getName()));
            errors.append("bad module name: expected='").append(mrid.getName()).append("' found='")
                    .append(md.getModuleRevisionId().getName()).append("'; ");
            ok = false;
        }
        if (mrid.getBranch() != null
                && !mrid.getBranch().equals(md.getModuleRevisionId().getBranch())) {
            Message.error(String.format("\t%s: bad branch name found in %s: expected='%s found='%s'",
                    getName(), ivyRef.getResource(), mrid.getBranch(),
                    md.getModuleRevisionId().getBranch()));
            errors.append("bad branch name: expected='").append(mrid.getBranch()).append("' found='")
                    .append(md.getModuleRevisionId().getBranch()).append("'; ");
            ok = false;
        }
        if (ivyRef.getRevision() != null && !ivyRef.getRevision().startsWith("working@")
                && !mrid.getRevision().equals(md.getModuleRevisionId().getRevision())) {
            ModuleRevisionId expectedMrid = ModuleRevisionId.newInstance(mrid, mrid.getRevision());
            if (!getSettings().getVersionMatcher().accept(expectedMrid, md)) {
                Message.error(String.format("\t%s: bad revision found in %s: expected='%s found='%s'",
                        getName(), ivyRef.getResource(), ivyRef.getRevision(),
                        md.getModuleRevisionId().getRevision()));
                errors.append("bad revision: expected='").append(ivyRef.getRevision())
                        .append("' found='").append(md.getModuleRevisionId().getRevision()).append("'; ");
                ok = false;
            }
        }
        if (!getSettings().getStatusManager().isStatus(md.getStatus())) {
            Message.error(String.format("\t%s: bad status found in %s: '%s'",
                    getName(), ivyRef.getResource(), md.getStatus()));
            errors.append("bad status: '").append(md.getStatus()).append("'; ");
            ok = false;
        }
        for (Map.Entry<String, String> extra : mrid.getExtraAttributes().entrySet()) {
            if (extra.getValue() != null
                    && !extra.getValue().equals(md.getExtraAttribute(extra.getKey()))) {
                String errorMsg = String.format("bad %s found in %s: expected='%s' found='%s'",
                        extra.getKey(), ivyRef.getResource(), extra.getValue(),
                        md.getExtraAttribute(extra.getKey()));
                Message.error("\t" + getName() + ": " + errorMsg);
                errors.append(errorMsg).append(";");
                ok = false;
            }
        }
        if (!ok) {
            throw new ParseException("inconsistent module descriptor file found in '"
                    + ivyRef.getResource() + "': " + errors, 0);
        }
    }

    /**
     * When the resolver has many choices, this function helps choosing one
     *
     * @param rress
     *            the list of resolved resource which the resolver found to fit the requirement
     * @param rmdparser
     *            the parser of module descriptor
     * @param mrid
     *            the module being resolved
     * @param date
     *            the current date
     * @return the selected resource
     */
    public ResolvedResource findResource(ResolvedResource[] rress, ResourceMDParser rmdparser,
            ModuleRevisionId mrid, Date date) {
        String name = getName();
        VersionMatcher versionMatcher = getSettings().getVersionMatcher();

        ResolvedResource found = null;
        List<ArtifactInfo> sorted = getLatestStrategy().sort(rress);
        List<String> rejected = new ArrayList<>();
        List<ModuleRevisionId> foundBlacklisted = new ArrayList<>();
        IvyContext context = IvyContext.getContext();

        ListIterator<ArtifactInfo> iter = sorted.listIterator(sorted.size());
        while (iter.hasPrevious()) {
            ResolvedResource rres = (ResolvedResource) iter.previous();
            // we start by filtering based on information already available,
            // even though we don't even know if the resource actually exist.
            // But checking for existence is most of the time more costly than checking
            // name, blacklisting and first level version matching
            if (filterNames(new ArrayList<>(Collections.singleton(rres.getRevision())))
                    .isEmpty()) {
                Message.debug("\t" + name + ": filtered by name: " + rres);
                continue;
            }
            ModuleRevisionId foundMrid = ModuleRevisionId.newInstance(mrid, rres.getRevision());

            ResolveData data = context.getResolveData();
            if (data != null && data.getReport() != null
                    && data.isBlacklisted(data.getReport().getConfiguration(), foundMrid)) {
                Message.debug("\t" + name + ": blacklisted: " + rres);
                rejected.add(rres.getRevision() + " (blacklisted)");
                foundBlacklisted.add(foundMrid);
                continue;
            }

            if (!versionMatcher.accept(mrid, foundMrid)) {
                Message.debug("\t" + name + ": rejected by version matcher: " + rres);
                rejected.add(rres.getRevision());
                continue;
            }
            if (rres.getResource() != null && !rres.getResource().exists()) {
                Message.debug("\t" + name + ": unreachable: " + rres + "; res="
                        + rres.getResource());
                rejected.add(rres.getRevision() + " (unreachable)");
                continue;
            }
            if (date != null && rres.getLastModified() > date.getTime()) {
                Message.verbose("\t" + name + ": too young: " + rres);
                rejected.add(rres.getRevision() + " (" + rres.getLastModified() + ")");
                continue;
            }
            if (versionMatcher.needModuleDescriptor(mrid, foundMrid)) {
                MDResolvedResource r = rmdparser.parse(rres.getResource(), rres.getRevision());
                if (r == null) {
                    Message.debug("\t" + name + ": impossible to get module descriptor resource: "
                            + rres);
                    rejected.add(rres.getRevision() + " (no or bad MD)");
                    continue;
                }
                ModuleDescriptor md = r.getResolvedModuleRevision().getDescriptor();
                if (md.isDefault()) {
                    Message.debug("\t" + name + ": default md rejected by version matcher"
                            + "requiring module descriptor: " + rres);
                    rejected.add(rres.getRevision() + " (MD)");
                    continue;
                }
                if (!versionMatcher.accept(mrid, md)) {
                    Message.debug("\t" + name + ": md rejected by version matcher: " + rres);
                    rejected.add(rres.getRevision() + " (MD)");
                    continue;
                }
                found = r;
            } else {
                found = rres;
            }

            if (found != null) {
                break;
            }
        }
        if (found == null && !rejected.isEmpty()) {
            logAttempt(rejected.toString());
        }
        if (found == null && !foundBlacklisted.isEmpty()) {
            // all acceptable versions have been blacklisted, this means that an unsolvable conflict
            // has been found
            DependencyDescriptor dd = context.getDependencyDescriptor();
            IvyNode parentNode = context.getResolveData().getNode(dd.getParentRevisionId());
            ConflictManager cm = parentNode.getConflictManager(mrid.getModuleId());
            cm.handleAllBlacklistedRevisions(dd, foundBlacklisted);
        }

        return found;
    }

    /**
     * Filters names before returning them in the findXXXNames or findTokenValues method.
     * <p>
     * Remember to call the super implementation when overriding this method.
     * </p>
     *
     * @param names
     *            the list to filter.
     * @return the filtered list
     */
    protected Collection<String> filterNames(Collection<String> names) {
        getSettings().filterIgnore(names);
        return names;
    }

    protected void clearIvyAttempts() {
        ivyattempts.clear();
        clearArtifactAttempts();
    }

    protected void logIvyAttempt(String attempt) {
        ivyattempts.add(attempt);
        Message.verbose("\t\ttried " + attempt);
    }

    protected void logArtifactAttempt(Artifact art, String attempt) {
        List<String> attempts = artattempts.get(art);
        if (attempts == null) {
            attempts = new ArrayList<>();
            artattempts.put(art, attempts);
        }
        attempts.add(attempt);
        Message.verbose("\t\ttried " + attempt);
    }

    protected void logAttempt(String attempt) {
        Artifact currentArtifact = IvyContext.getContext().get(getName() + ".artifact");
        if (currentArtifact == null) {
            logIvyAttempt(attempt);
        } else {
            logArtifactAttempt(currentArtifact, attempt);
        }
    }

    @Override
    public void reportFailure() {
        Message.warn("==== " + getName() + ": tried");
        for (String m : ivyattempts) {
            Message.warn("  " + m);
        }
        for (Map.Entry<Artifact, List<String>> entry : artattempts.entrySet()) {
            List<String> attempts = entry.getValue();
            if (attempts != null) {
                Message.warn("  -- artifact " + entry.getKey() + ":");
                for (String m : attempts) {
                    Message.warn("  " + m);
                }
            }
        }
    }

    @Override
    public void reportFailure(Artifact art) {
        Message.warn("==== " + getName() + ": tried");
        List<String> attempts = artattempts.get(art);
        if (attempts != null) {
            for (String m : attempts) {
                Message.warn("  " + m);
            }
        }
    }

    protected boolean acceptLatest() {
        return true;
    }

    public DownloadReport download(Artifact[] artifacts, DownloadOptions options) {
        RepositoryCacheManager cacheManager = getRepositoryCacheManager();

        clearArtifactAttempts();
        DownloadReport dr = new DownloadReport();
        for (Artifact artifact : artifacts) {
            ArtifactDownloadReport adr = cacheManager.download(artifact, artifactResourceResolver,
                downloader, getCacheDownloadOptions(options));
            if (DownloadStatus.FAILED == adr.getDownloadStatus()) {
                if (!ArtifactDownloadReport.MISSING_ARTIFACT.equals(adr.getDownloadDetails())) {
                    Message.warn("\t" + adr);
                }
            } else if (DownloadStatus.NO == adr.getDownloadStatus()) {
                Message.verbose("\t" + adr);
            } else if (LogOptions.LOG_QUIET.equals(options.getLog())) {
                Message.verbose("\t" + adr);
            } else {
                Message.info("\t" + adr);
            }
            dr.addArtifactReport(adr);
            checkInterrupted();
        }
        return dr;
    }

    protected void clearArtifactAttempts() {
        artattempts.clear();
    }

    @Override
    public ArtifactDownloadReport download(final ArtifactOrigin origin, DownloadOptions options) {
        Checks.checkNotNull(origin, "origin");
        return getRepositoryCacheManager().download(origin.getArtifact(),
            new ArtifactResourceResolver() {
                public ResolvedResource resolve(Artifact artifact) {
                    try {
                        Resource resource = getResource(origin.getLocation());
                        if (resource != null) {
                            String revision = origin.getArtifact().getModuleRevisionId().getRevision();
                            return new ResolvedResource(resource, revision);
                        }
                    } catch (IOException e) {
                        Message.debug(e);
                    }
                    return null;
                }
            }, downloader, getCacheDownloadOptions(options));
    }

    protected abstract Resource getResource(String source) throws IOException;

    @Override
    public boolean exists(Artifact artifact) {
        ResolvedResource artifactRef = getArtifactRef(artifact, null);
        return artifactRef != null && artifactRef.getResource().exists();
    }

    @Override
    public ArtifactOrigin locate(Artifact artifact) {
        ArtifactOrigin origin = getRepositoryCacheManager().getSavedArtifactOrigin(
            toSystem(artifact));
        if (!ArtifactOrigin.isUnknown(origin)) {
            return origin;
        }
        ResolvedResource artifactRef = getArtifactRef(artifact, null);
        if (artifactRef != null && artifactRef.getResource().exists()) {
            return new ArtifactOrigin(artifact, artifactRef.getResource().isLocal(), artifactRef
                    .getResource().getName());
        }
        return null;
    }

    protected long getPublicationDate(ModuleDescriptor md, DependencyDescriptor dd, ResolveData data) {
        if (md.getPublicationDate() != null) {
            return md.getPublicationDate().getTime();
        }
        ResolvedResource artifactRef = findFirstArtifactRef(md, dd, data);
        if (artifactRef != null) {
            return artifactRef.getLastModified();
        }
        return -1;
    }

    @Override
    public String toString() {
        return getName();
    }

    @Override
    public String[] listTokenValues(String token, Map<String, String> otherTokenValues) {
        Collection<String> ret = findNames(otherTokenValues, token);
        return ret.toArray(new String[ret.size()]);
    }

    @Override
    public OrganisationEntry[] listOrganisations() {
        Collection<String> names = findNames(Collections.<String, String> emptyMap(),
            IvyPatternHelper.ORGANISATION_KEY);
        List<OrganisationEntry> ret = new ArrayList<>(names.size());
        for (String org : names) {
            ret.add(new OrganisationEntry(this, org));
        }
        return ret.toArray(new OrganisationEntry[names.size()]);
    }

    @Override
    public ModuleEntry[] listModules(OrganisationEntry org) {
        Map<String, String> tokenValues = new HashMap<>();
        tokenValues.put(IvyPatternHelper.ORGANISATION_KEY, org.getOrganisation());
        Collection<String> names = findNames(tokenValues, IvyPatternHelper.MODULE_KEY);
        List<ModuleEntry> ret = new ArrayList<>(names.size());
        for (String name : names) {
            ret.add(new ModuleEntry(org, name));
        }
        return ret.toArray(new ModuleEntry[names.size()]);
    }

    @Override
    public RevisionEntry[] listRevisions(ModuleEntry mod) {
        Map<String, String> tokenValues = new HashMap<>();
        tokenValues.put(IvyPatternHelper.ORGANISATION_KEY, mod.getOrganisation());
        tokenValues.put(IvyPatternHelper.MODULE_KEY, mod.getModule());
        Collection<String> names = findNames(tokenValues, IvyPatternHelper.REVISION_KEY);
        List<RevisionEntry> ret = new ArrayList<>(names.size());
        for (String name : names) {
            ret.add(new RevisionEntry(mod, name));
        }
        return ret.toArray(new RevisionEntry[names.size()]);
    }

    protected abstract Collection<String> findNames(Map<String, String> tokenValues, String token);

    protected ResolvedResource findFirstArtifactRef(ModuleDescriptor md, DependencyDescriptor dd,
            ResolveData data) {
        for (String configName : md.getConfigurationsNames()) {
            for (Artifact artifact : md.getArtifacts(configName)) {
                ResolvedResource ret = getArtifactRef(artifact, data.getDate());
                if (ret != null) {
                    return ret;
                }
            }
        }
        return null;
    }

    protected long getAndCheck(Resource resource, File dest) throws IOException {
        long size = get(resource, dest);
        for (String checksum : getChecksumAlgorithms()) {
            if (check(resource, dest, checksum)) {
                break;
            }
        }
        return size;
    }

    /**
     * Checks the given resource checksum if a checksum resource exists.
     *
     * @param resource
     *            the resource to check
     * @param dest
     *            the file where the resource has been downloaded
     * @param algorithm
     *            the checksum algorithm to use
     * @return true if the checksum has been successfully checked, false if the checksum wasn't
     *         available
     * @throws IOException
     *             if a checksum exist but do not match the downloaded file checksum
     */
    private boolean check(Resource resource, File dest, String algorithm) throws IOException {
        if (!ChecksumHelper.isKnownAlgorithm(algorithm)) {
            throw new IllegalArgumentException("Unknown checksum algorithm: " + algorithm);
        }

        Resource csRes = resource.clone(resource.getName() + "." + algorithm);
        if (csRes.exists()) {
            Message.debug(algorithm + " file found for " + resource + ": checking...");
            File csFile = File.createTempFile("ivytmp", algorithm);
            try {
                get(csRes, csFile);
                try {
                    ChecksumHelper.check(dest, csFile, algorithm);
                    Message.verbose(algorithm + " OK for " + resource);
                    return true;
                } catch (IOException ex) {
                    dest.delete();
                    throw ex;
                }
            } finally {
                csFile.delete();
            }
        } else {
            return false;
        }
    }

    protected ResolvedResource getArtifactRef(Artifact artifact, Date date) {
        IvyContext.getContext().set(getName() + ".artifact", artifact);
        try {
            ResolvedResource ret = findArtifactRef(artifact, date);
            if (ret == null && artifact.getUrl() != null) {
                URL url = artifact.getUrl();
                Message.verbose("\tusing url for " + artifact + ": " + url);
                logArtifactAttempt(artifact, url.toExternalForm());
                Resource resource;
                if ("file".equals(url.getProtocol())) {
                    File f;
                    try {
                        f = new File(new URI(url.toExternalForm()));
                    } catch (URISyntaxException e) {
                        // unexpected, try to get the best of it
                        f = new File(url.getPath());
                    }
                    resource = new FileResource(new FileRepository(), f);
                } else {
                    resource = new URLResource(url, this.getTimeoutConstraint());
                }
                ret = new ResolvedResource(resource, artifact.getModuleRevisionId().getRevision());
            }
            return ret;
        } finally {
            IvyContext.getContext().set(getName() + ".artifact", null);
        }
    }

    public ResolvedResource doFindArtifactRef(Artifact artifact, Date date) {
        return findArtifactRef(artifact, date);
    }

    protected abstract ResolvedResource findArtifactRef(Artifact artifact, Date date);

    protected abstract long get(Resource resource, File dest) throws IOException;

    public boolean isCheckconsistency() {
        return checkconsistency;
    }

    public void setCheckconsistency(boolean checkConsistency) {
        checkconsistency = checkConsistency;
    }

    public void setForce(boolean force) {
        this.force = force;
    }

    public boolean isForce() {
        return force;
    }

    public boolean isAllownomd() {
        return allownomd;
    }

    public void setAllownomd(boolean b) {
        Message.deprecated("allownomd is deprecated, please use descriptor=\""
                + (b ? DESCRIPTOR_OPTIONAL : DESCRIPTOR_REQUIRED) + "\" instead");
        allownomd = b;
    }

    /**
     * Sets the module descriptor presence rule. Should be one of {@link #DESCRIPTOR_REQUIRED} or
     * {@link #DESCRIPTOR_OPTIONAL}.
     *
     * @param descriptorRule
     *            the descriptor rule to use with this resolver.
     */
    public void setDescriptor(String descriptorRule) {
        switch (descriptorRule) {
            case DESCRIPTOR_REQUIRED:
                allownomd = false;
                break;
            case DESCRIPTOR_OPTIONAL:
                allownomd = true;
                break;
            default:
                throw new IllegalArgumentException("unknown descriptor rule '" + descriptorRule
                        + "'. Allowed rules are: "
                        + Arrays.asList(DESCRIPTOR_REQUIRED, DESCRIPTOR_OPTIONAL));
        }
    }

    public String[] getChecksumAlgorithms() {
        String csDef = (checksums == null) ? getSettings().getVariable("ivy.checksums") : checksums;
        if (csDef == null) {
            return new String[0];
        }
        // csDef is a comma separated list of checksum algorithms to use with this resolver
        // we parse and return it as a String[]
        List<String> algos = new ArrayList<>();
        for (String cs : splitToArray(csDef)) {
            if (!cs.isEmpty() && !"none".equals(cs)) {
                algos.add(cs);
            }
        }
        return algos.toArray(new String[algos.size()]);
    }

    public void setChecksums(String checksums) {
        this.checksums = checksums;
    }

    private final ArtifactResourceResolver artifactResourceResolver = new ArtifactResourceResolver() {
        public ResolvedResource resolve(Artifact artifact) {
            artifact = fromSystem(artifact);
            return getArtifactRef(artifact, null);
        }
    };

    private final ResourceDownloader downloader = new ResourceDownloader() {
        public void download(Artifact artifact, Resource resource, File dest) throws IOException {
            if (dest.exists()) {
                dest.delete();
            }
            File part = new File(dest.getAbsolutePath() + ".part");
            if (resource.getName().equals(String.valueOf(artifact.getUrl()))) {
                if (part.getParentFile() != null) {
                    part.getParentFile().mkdirs();
                }
                extartifactrep.get(resource.getName(), part);
            } else {
                getAndCheck(resource, part);
            }
            if (!part.renameTo(dest)) {
                throw new IOException("impossible to move part file to definitive one: " + part
                        + " -> " + dest);
            }

        }
    };

}
