/*
 *  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.
 *
 */
package org.apache.ivy.core.module.descriptor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.ivy.core.module.id.ArtifactId;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.plugins.matcher.MatcherHelper;
import org.apache.ivy.plugins.namespace.NameSpaceHelper;
import org.apache.ivy.plugins.namespace.Namespace;
import org.apache.ivy.plugins.namespace.NamespaceTransformer;
import org.apache.ivy.util.Checks;

/**
 * This class can be used as the default implementation for DependencyDescriptor. It implements
 * required methods and enables to fill dependency information with the addDependencyConfiguration
 * method.
 */
public class DefaultDependencyDescriptor implements DependencyDescriptor {
    private static final Pattern SELF_FALLBACK_PATTERN = Pattern
            .compile("@(\\+[^\\(]+)?(\\(.*\\))?");

    private static final Pattern THIS_FALLBACK_PATTERN = Pattern
            .compile("#(\\+[^\\(]+)?(\\(.*\\))?");

    /**
     * Transforms the given dependency descriptor of the given namespace and return a new dependency
     * descriptor in the system namespace. <i>Note that exclude rules are not converted in system
     * namespace, because they aren't transformable (the name space hasn't the ability to convert
     * regular expressions). However, method doesExclude will work with system artifacts.</i>
     *
     * @param dd DependencyDescriptor
     * @param ns Namespace
     * @return DependencyDescriptor
     */
    public static DependencyDescriptor transformInstance(DependencyDescriptor dd, Namespace ns) {
        NamespaceTransformer t = ns.getToSystemTransformer();
        if (t.isIdentity()) {
            return dd;
        }
        DefaultDependencyDescriptor newdd = transformInstance(dd, t, false);
        newdd.namespace = ns;
        return newdd;
    }

    /**
     * Transforms a dependency descriptor using the given transformer. Note that no namespace info
     * will be attached to the transformed dependency descriptor, so calling doesExclude is not
     * recommended (doesExclude only works when namespace is properly set)
     *
     * @param dd DependencyDescriptor
     * @param t NamespaceTransformer
     * @param fromSystem boolean
     * @return DefaultDependencyDescriptor
     */
    public static DefaultDependencyDescriptor transformInstance(DependencyDescriptor dd,
            NamespaceTransformer t, boolean fromSystem) {
        ModuleRevisionId transformParentId = t.transform(dd.getParentRevisionId());
        ModuleRevisionId transformMrid = t.transform(dd.getDependencyRevisionId());
        ModuleRevisionId transformDynamicMrid = t.transform(dd
                .getDynamicConstraintDependencyRevisionId());
        DefaultDependencyDescriptor newdd = new DefaultDependencyDescriptor(null, transformMrid,
                transformDynamicMrid, dd.isForce(), dd.isChanging(), dd.isTransitive());

        newdd.parentId = transformParentId;
        ModuleRevisionId sourceModule = dd.getSourceModule();
        if (sourceModule != null) {
            newdd.sourceModule = t.transform(sourceModule);
        }

        String[] moduleConfs = dd.getModuleConfigurations();
        if (moduleConfs.length == 1 && "*".equals(moduleConfs[0])) {
            if (dd instanceof DefaultDependencyDescriptor) {
                DefaultDependencyDescriptor ddd = (DefaultDependencyDescriptor) dd;
                newdd.confs = new LinkedHashMap<>(ddd.confs);
                newdd.setExcludeRules(new LinkedHashMap<>(ddd.getExcludeRules()));
                newdd.setIncludeRules(new LinkedHashMap<>(ddd.getIncludeRules()));
                newdd.setDependencyArtifacts(new LinkedHashMap<>(ddd.getDependencyArtifacts()));
            } else {
                throw new IllegalArgumentException(
                        "dependency descriptor transformation does not support * module confs "
                                + "with descriptors which aren't DefaultDependencyDescriptor");
            }
        } else {
            for (String moduleConf : moduleConfs) {
                newdd.confs.put(moduleConf,
                    new ArrayList<>(Arrays.asList(dd.getDependencyConfigurations(moduleConf))));
                newdd.getExcludeRules().put(moduleConf,
                    new ArrayList<>(Arrays.asList(dd.getExcludeRules(moduleConf))));
                newdd.getIncludeRules().put(moduleConf,
                    new ArrayList<>(Arrays.asList(dd.getIncludeRules(moduleConf))));
                newdd.getDependencyArtifacts().put(moduleConf,
                    new ArrayList<>(Arrays.asList(dd.getDependencyArtifacts(moduleConf))));
            }
        }
        if (fromSystem) {
            newdd.asSystem = dd;
        }
        return newdd;
    }

    private final ModuleRevisionId revId;

    private ModuleRevisionId dynamicRevId;

    private Map<String, List<String>> confs = new LinkedHashMap<>();

    // Map (String masterConf -> Collection(DependencyArtifactDescriptor))
    // initialized on demand only for memory consumption reasons
    private Map<String, Collection<DependencyArtifactDescriptor>> dependencyArtifacts;

    // Map (String masterConf -> Collection(IncludeRule))
    // initialized on demand only for memory consumption reasons
    private Map<String, Collection<IncludeRule>> includeRules;

    // Map (String masterConf -> Collection(ExcludeRule))
    // initialized on demand only for memory consumption reasons
    private Map<String, Collection<ExcludeRule>> excludeRules;

    /**
     * Used to indicate that this revision must be used in case of conflicts, independently of
     * conflicts manager
     */
    private boolean isForce;

    /**
     * Used to indicate that the dependency is a changing one, i.e. that ivy should not rely on the
     * version to know if it can trust artifacts in cache
     */
    private boolean isChanging;

    private ModuleRevisionId parentId;

    private boolean isTransitive = true;

    /**
     * This namespace should be used to check
     */
    private Namespace namespace = null;

    private final ModuleDescriptor md;

    private DependencyDescriptor asSystem = this;

    private ModuleRevisionId sourceModule;

    private DefaultDependencyDescriptor(DefaultDependencyDescriptor dd, ModuleRevisionId revision) {
        Checks.checkNotNull(dd, "dd");
        Checks.checkNotNull(revision, "revision");

        if (!revision.getModuleId().equals(dd.getDependencyId())) {
            throw new IllegalArgumentException(
                    "new ModuleRevisionId MUST have the same ModuleId as original one."
                            + " original = " + dd.getDependencyId() + " new = "
                            + revision.getModuleId());
        }
        md = dd.md;
        parentId = dd.parentId;
        revId = revision;
        dynamicRevId = dd.dynamicRevId;
        isForce = dd.isForce;
        isChanging = dd.isChanging;
        isTransitive = dd.isTransitive;
        namespace = dd.namespace;
        confs.putAll(dd.confs);
        excludeRules = (dd.excludeRules == null) ? null : new LinkedHashMap<>(dd.excludeRules);
        includeRules = (dd.includeRules == null) ? null : new LinkedHashMap<>(dd.includeRules);
        dependencyArtifacts = (dd.dependencyArtifacts == null) ? null
                : new LinkedHashMap<>(dd.dependencyArtifacts);
        sourceModule = dd.sourceModule;
    }

    public DefaultDependencyDescriptor(ModuleDescriptor md, ModuleRevisionId mrid, boolean force,
            boolean changing, boolean transitive) {
        this(md, mrid, mrid, force, changing, transitive);
    }

    public DefaultDependencyDescriptor(ModuleRevisionId mrid, boolean force) {
        this(mrid, force, false);
    }

    public DefaultDependencyDescriptor(ModuleRevisionId mrid, boolean force, boolean changing) {
        this(null, mrid, mrid, force, changing, true);
    }

    public DefaultDependencyDescriptor(ModuleDescriptor md, ModuleRevisionId mrid,
            ModuleRevisionId dynamicConstraint, boolean force, boolean changing, boolean transitive) {
        Checks.checkNotNull(mrid, "mrid");
        Checks.checkNotNull(dynamicConstraint, "dynamicConstraint");

        this.md = md;
        revId = mrid;
        dynamicRevId = dynamicConstraint;
        isForce = force;
        isChanging = changing;
        isTransitive = transitive;
        sourceModule = md == null ? null : md.getModuleRevisionId();
    }

    public ModuleId getDependencyId() {
        return getDependencyRevisionId().getModuleId();
    }

    public ModuleRevisionId getDependencyRevisionId() {
        return revId;
    }

    public ModuleRevisionId getDynamicConstraintDependencyRevisionId() {
        return dynamicRevId;
    }

    public String[] getModuleConfigurations() {
        return confs.keySet().toArray(new String[confs.keySet().size()]);
    }

    public String[] getDependencyConfigurations(String moduleConfiguration) {
        return getDependencyConfigurations(moduleConfiguration, moduleConfiguration);
    }

    /**
     * Return the dependency configurations mapped to the given moduleConfiguration, actually
     * resolved because of the given requestedConfiguration
     * <p>
     * Usually requestedConfiguration and moduleConfiguration are the same, except when a conf
     * extends another, then the moduleConfiguration is the configuration currently resolved (the
     * extended one), and requestedConfiguration is the one actually requested initially (the
     * extending one). Both moduleConfiguration and requestedConfiguration are configurations of the
     * caller, the array returned is composed of the required configurations of the dependency
     * described by this descriptor.
     * </p>
     *
     * @param moduleConfiguration String
     * @param requestedConfiguration String
     * @return String[]
     */
    public String[] getDependencyConfigurations(String moduleConfiguration,
            String requestedConfiguration) {
        if (md != null) {
            Configuration c = md.getConfiguration(moduleConfiguration);
            if (c instanceof ConfigurationIntersection) {
                ConfigurationIntersection intersection = (ConfigurationIntersection) c;
                Set<String> intersectedDepConfs = new HashSet<>();
                for (String intersect : intersection.getIntersectedConfigurationNames()) {
                    Collection<String> depConfs = getDependencyConfigurationsIncludingExtending(
                        intersect, requestedConfiguration);
                    if (intersectedDepConfs.isEmpty()) {
                        intersectedDepConfs.addAll(depConfs);
                    } else {
                        if (intersectedDepConfs.contains("*")) {
                            intersectedDepConfs.remove("*");
                            intersectedDepConfs.addAll(depConfs);
                        } else if (depConfs.contains("*")) {
                            // nothing to do, intersection of 'something'
                            // with 'everything' is 'something'
                        } else {
                            Set<String> intersectedDepConfsCopy = intersectedDepConfs;
                            intersectedDepConfs = new HashSet<>();
                            for (String intersectedDepConf : intersectedDepConfsCopy) {
                                if (depConfs.contains(intersectedDepConf)) {
                                    // the conf is present in both sets,
                                    // so it is in the intersection
                                    intersectedDepConfs.add(intersectedDepConf);
                                    continue;
                                }
                                /*
                                 * we do not handle special confs like *!sg or [cond]* in right hand
                                 * confs yet: it would require supporting parenthesis grouping in
                                 * configurations intersection interpretation
                                 *
                                 * for (String depConf : depConfs) {
                                 *    if (depConf.startsWith("*")) {
                                 *       if (intersectedDepConf.contains("(" + depConf + ")")) {
                                 *          intersectedDepConfs.add(intersectedDepConf);
                                 *       } else {
                                 *          intersectedDepConfs.add("(" + intersectedDepConf + ")+(" + depConf + ")");
                                 *       }
                                 *    } else if (intersectedDepConf.startsWith("*")) {
                                 *       if (depConf.contains("(" + intersectedDepConf + ")")) {
                                 *          intersectedDepConfs.add(depConf);
                                 *       } else {
                                 *          intersectedDepConfs.add(depConf + "+" + intersectedDepConf);
                                 *       }
                                 *    }
                                 * }
                                 */
                            }
                        }
                    }
                }
                List<String> confsList = confs.get(moduleConfiguration);
                if (confsList != null) {
                    intersectedDepConfs.addAll(confsList);
                }
                if (intersectedDepConfs.isEmpty()) {
                    List<String> defConfs = confs.get("*");
                    if (defConfs != null) {
                        for (String mappedConf : defConfs) {
                            if (mappedConf != null && mappedConf.startsWith("@+")) {
                                return new String[] {moduleConfiguration + mappedConf.substring(1)};
                            } else if (mappedConf != null && mappedConf.equals("@")) {
                                return new String[] {moduleConfiguration};
                            }
                        }
                    }
                }
                return intersectedDepConfs.toArray(new String[intersectedDepConfs.size()]);
            } else if (c instanceof ConfigurationGroup) {
                ConfigurationGroup group = (ConfigurationGroup) c;
                Set<String> groupDepConfs = new HashSet<>();
                for (String member : group.getMembersConfigurationNames()) {
                    Collection<String> depConfs = getDependencyConfigurationsIncludingExtending(
                        member, requestedConfiguration);
                    groupDepConfs.addAll(depConfs);
                }
                return groupDepConfs.toArray(new String[groupDepConfs.size()]);
            }
        }

        List<String> confsList = confs.get(moduleConfiguration);
        if (confsList == null) {
            // there is no mapping defined for this configuration, add the 'other' mappings.
            confsList = confs.get("%");
        }
        List<String> defConfs = confs.get("*");
        Collection<String> ret = new LinkedHashSet<>();
        if (confsList != null) {
            ret.addAll(confsList);
        }
        if (defConfs != null) {
            ret.addAll(defConfs);
        }

        Collection<String> replacedRet = new LinkedHashSet<>();
        for (String c : ret) {
            String replacedConf = replaceSelfFallbackPattern(c, moduleConfiguration);
            if (replacedConf == null) {
                replacedConf = replaceThisFallbackPattern(c, requestedConfiguration);
            }
            if (replacedConf != null) {
                c = replacedConf;
            }
            replacedRet.add(c);
        }
        ret = replacedRet;
        if (ret.remove("*")) {
            StringBuilder r = new StringBuilder("*");
            // merge excluded configurations as one conf like *!A!B
            for (String c : ret) {
                if (c.startsWith("!")) {
                    r.append(c);
                }
            }
            return new String[] {r.toString()};
        }
        return ret.toArray(new String[ret.size()]);
    }

    private Collection<String> getDependencyConfigurationsIncludingExtending(String conf,
            String requestedConfiguration) {
        Set<String> allDepConfs = new LinkedHashSet<>(Arrays.asList(getDependencyConfigurations(conf,
                requestedConfiguration)));

        Collection<Configuration> extendingConfs = Configuration.findConfigurationExtending(conf,
            md.getConfigurations());
        for (Configuration extendingConf : extendingConfs) {
            allDepConfs.addAll(Arrays.asList(
                getDependencyConfigurations(extendingConf.getName(), requestedConfiguration)));
        }
        return allDepConfs;
    }

    protected static String replaceSelfFallbackPattern(final String conf,
            final String moduleConfiguration) {
        return replaceFallbackConfigurationPattern(SELF_FALLBACK_PATTERN, conf, moduleConfiguration);
    }

    protected static String replaceThisFallbackPattern(final String conf,
            final String requestedConfiguration) {
        return replaceFallbackConfigurationPattern(THIS_FALLBACK_PATTERN, conf,
            requestedConfiguration);
    }

    /**
     * Replaces fallback patterns with correct values if fallback pattern exists.
     *
     * @param pattern
     *            pattern to look for
     * @param conf
     *            configuration mapping from dependency element
     * @param moduleConfiguration
     *            module's configuration to use for replacement
     * @return Replaced string if pattern matched. Otherwise null.
     */
    protected static String replaceFallbackConfigurationPattern(final Pattern pattern,
            final String conf, final String moduleConfiguration) {
        Matcher matcher = pattern.matcher(conf);
        if (matcher.matches()) {
            String mappedConf = moduleConfiguration;
            if (matcher.group(1) != null) {
                mappedConf += matcher.group(1);
            }
            if (matcher.group(2) != null) {
                mappedConf += matcher.group(2);
            }
            return mappedConf;
        }
        return null;
    }

    public String[] getDependencyConfigurations(String[] moduleConfigurations) {
        Set<String> confs = new LinkedHashSet<>();
        for (String moduleConfiguration : moduleConfigurations) {
            confs.addAll(Arrays.asList(getDependencyConfigurations(moduleConfiguration)));
        }
        if (confs.contains("*")) {
            return new String[] {"*"};
        }
        return confs.toArray(new String[confs.size()]);
    }

    public DependencyArtifactDescriptor[] getDependencyArtifacts(String moduleConfiguration) {
        Collection<DependencyArtifactDescriptor> artifacts = getCollectionForConfiguration(
            moduleConfiguration, dependencyArtifacts);
        return artifacts.toArray(new DependencyArtifactDescriptor[artifacts.size()]);
    }

    public IncludeRule[] getIncludeRules(String moduleConfiguration) {
        Collection<IncludeRule> rules = getCollectionForConfiguration(moduleConfiguration,
            includeRules);
        return rules.toArray(new IncludeRule[rules.size()]);
    }

    public ExcludeRule[] getExcludeRules(String moduleConfiguration) {
        Collection<ExcludeRule> rules = getCollectionForConfiguration(moduleConfiguration,
            excludeRules);
        return rules.toArray(new ExcludeRule[rules.size()]);
    }

    private <T> Set<T> getCollectionForConfiguration(String moduleConfiguration,
            Map<String, Collection<T>> collectionMap) {
        if (collectionMap == null || collectionMap.isEmpty()) {
            return Collections.emptySet();
        }
        Collection<T> artifacts = collectionMap.get(moduleConfiguration);
        Collection<T> defArtifacts = collectionMap.get("*");
        Set<T> ret = new LinkedHashSet<>();
        if (artifacts != null) {
            ret.addAll(artifacts);
        }
        if (defArtifacts != null) {
            ret.addAll(defArtifacts);
        }
        return ret;
    }

    public DependencyArtifactDescriptor[] getDependencyArtifacts(String[] moduleConfigurations) {
        Set<DependencyArtifactDescriptor> artifacts = new LinkedHashSet<>();
        for (String moduleConfiguration : moduleConfigurations) {
            artifacts.addAll(Arrays.asList(getDependencyArtifacts(moduleConfiguration)));
        }
        return artifacts.toArray(new DependencyArtifactDescriptor[artifacts.size()]);
    }

    public IncludeRule[] getIncludeRules(String[] moduleConfigurations) {
        Set<IncludeRule> rules = new LinkedHashSet<>();
        for (String moduleConfiguration : moduleConfigurations) {
            rules.addAll(Arrays.asList(getIncludeRules(moduleConfiguration)));
        }
        return rules.toArray(new IncludeRule[rules.size()]);
    }

    public ExcludeRule[] getExcludeRules(String[] moduleConfigurations) {
        Set<ExcludeRule> rules = new LinkedHashSet<>();
        for (String moduleConfiguration : moduleConfigurations) {
            rules.addAll(Arrays.asList(getExcludeRules(moduleConfiguration)));
        }
        return rules.toArray(new ExcludeRule[rules.size()]);
    }

    public DependencyArtifactDescriptor[] getAllDependencyArtifacts() {
        if (dependencyArtifacts == null) {
            return new DependencyArtifactDescriptor[0];
        }
        Set<DependencyArtifactDescriptor> ret = mergeAll(dependencyArtifacts);
        return ret.toArray(new DependencyArtifactDescriptor[ret.size()]);
    }

    public IncludeRule[] getAllIncludeRules() {
        if (includeRules == null) {
            return new IncludeRule[0];
        }
        Set<IncludeRule> ret = mergeAll(includeRules);
        return ret.toArray(new IncludeRule[ret.size()]);
    }

    public ExcludeRule[] getAllExcludeRules() {
        if (excludeRules == null) {
            return new ExcludeRule[0];
        }
        Set<ExcludeRule> ret = mergeAll(excludeRules);
        return ret.toArray(new ExcludeRule[ret.size()]);
    }

    private <T> Set<T> mergeAll(Map<String, Collection<T>> artifactsMap) {
        Set<T> ret = new LinkedHashSet<>();
        for (Collection<T> artifacts : artifactsMap.values()) {
            ret.addAll(artifacts);
        }
        return ret;
    }

    public void addDependencyConfiguration(String masterConf, String depConf) {
        if (md != null && !"*".equals(masterConf) && !"%".equals(masterConf)) {
            Configuration config;
            if (masterConf.startsWith("!")) {
                config = md.getConfiguration(masterConf.substring(1));
            } else {
                config = md.getConfiguration(masterConf);
            }
            if (config == null) {
                throw new IllegalArgumentException("Cannot add dependency '" + revId
                        + "' to configuration '" + masterConf + "' of module "
                        + md.getModuleRevisionId() + " because this configuration doesn't exist!");
            }
            if (config instanceof ConfigurationGroup) {
                ConfigurationGroup group = (ConfigurationGroup) config;
                for (String member : group.getMembersConfigurationNames()) {
                    addDependencyConfiguration(member, depConf);
                }
                return;
            }
        }

        List<String> confsList = confs.get(masterConf);
        if (confsList == null) {
            confsList = new ArrayList<>();
            confs.put(masterConf, confsList);
        }
        if (!confsList.contains(depConf)) {
            confsList.add(depConf);
        }
    }

    public void addDependencyArtifact(String masterConf, DependencyArtifactDescriptor dad) {
        addObjectToConfiguration(masterConf, dad, getDependencyArtifacts());
    }

    public void addIncludeRule(String masterConf, IncludeRule rule) {
        addObjectToConfiguration(masterConf, rule, getIncludeRules());
    }

    public void addExcludeRule(String masterConf, ExcludeRule rule) {
        addObjectToConfiguration(masterConf, rule, getExcludeRules());
    }

    private <T> void addObjectToConfiguration(String callerConf, T toAdd,
            Map<String, Collection<T>> confsMap) {
        Collection<T> col = confsMap.get(callerConf);
        if (col == null) {
            col = new ArrayList<>();
            confsMap.put(callerConf, col);
        }
        col.add(toAdd);
    }

    /**
     * only works when namespace is properly set. The behaviour is not specified if namespace is not
     * set.
     *
     * @param moduleConfigurations String[]
     * @param artifactId ditto
     * @return boolean
     */
    public boolean doesExclude(String[] moduleConfigurations, ArtifactId artifactId) {
        if (namespace != null) {
            artifactId = NameSpaceHelper
                    .transform(artifactId, namespace.getFromSystemTransformer());
        }
        for (ExcludeRule rule : getExcludeRules(moduleConfigurations)) {
            if (MatcherHelper.matches(rule.getMatcher(), rule.getId(), artifactId)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if this descriptor contains any exclusion rule
     */
    public boolean canExclude() {
        return excludeRules != null && !excludeRules.isEmpty();
    }

    public String toString() {
        return "dependency: " + revId + " " + confs;
    }

    public boolean isForce() {
        return isForce;
    }

    public ModuleRevisionId getParentRevisionId() {
        return md != null ? md.getResolvedModuleRevisionId() : parentId;
    }

    public boolean isChanging() {
        return isChanging;
    }

    public boolean isTransitive() {
        return isTransitive;
    }

    public Namespace getNamespace() {
        return namespace;
    }

    public String getAttribute(String attName) {
        return revId.getAttribute(attName);
    }

    public Map<String, String> getAttributes() {
        return revId.getAttributes();
    }

    public String getExtraAttribute(String attName) {
        return revId.getExtraAttribute(attName);
    }

    public Map<String, String> getExtraAttributes() {
        return revId.getExtraAttributes();
    }

    public Map<String, String> getQualifiedExtraAttributes() {
        return revId.getQualifiedExtraAttributes();
    }

    public DependencyDescriptor asSystem() {
        return asSystem;
    }

    private void setDependencyArtifacts(
            Map<String, Collection<DependencyArtifactDescriptor>> dependencyArtifacts) {
        this.dependencyArtifacts = dependencyArtifacts;
    }

    private Map<String, Collection<DependencyArtifactDescriptor>> getDependencyArtifacts() {
        if (dependencyArtifacts == null) {
            dependencyArtifacts = new LinkedHashMap<>();
        }
        return dependencyArtifacts;
    }

    private void setIncludeRules(Map<String, Collection<IncludeRule>> includeRules) {
        this.includeRules = includeRules;
    }

    private Map<String, Collection<IncludeRule>> getIncludeRules() {
        if (includeRules == null) {
            includeRules = new LinkedHashMap<>();
        }
        return includeRules;
    }

    private void setExcludeRules(Map<String, Collection<ExcludeRule>> excludeRules) {
        this.excludeRules = excludeRules;
    }

    private Map<String, Collection<ExcludeRule>> getExcludeRules() {
        if (excludeRules == null) {
            excludeRules = new LinkedHashMap<>();
        }
        return excludeRules;
    }

    public ModuleRevisionId getSourceModule() {
        return sourceModule;
    }

    public DependencyDescriptor clone(ModuleRevisionId revision) {
        return new DefaultDependencyDescriptor(this, revision);
    }
}
