/*
 *  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.core.module.descriptor;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
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.core.module.id.ModuleRules;
import org.apache.ivy.core.module.status.StatusManager;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.matcher.MapMatcher;
import org.apache.ivy.plugins.matcher.MatcherHelper;
import org.apache.ivy.plugins.matcher.PatternMatcher;
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.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorParser;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.version.VersionMatcher;
import org.apache.ivy.util.Message;

import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PUBLIC;

import static org.apache.ivy.core.module.descriptor.Configuration.findConfigurationExtending;

/**
 *
 */
public class DefaultModuleDescriptor implements ModuleDescriptor {

    public static DefaultModuleDescriptor newDefaultInstance(ModuleRevisionId mrid) {
        return newDefaultInstance(mrid, null);
    }

    public static DefaultModuleDescriptor newCallerInstance(ModuleRevisionId mrid, String[] confs,
            boolean transitive, boolean changing) {
        DefaultModuleDescriptor moduleDescriptor = new DefaultModuleDescriptor(
                ModuleRevisionId.newInstance(mrid.getOrganisation(), mrid.getName() + "-caller",
                    "working"), "integration", null, true);
        for (String conf : confs) {
            moduleDescriptor.addConfiguration(new Configuration(conf));
        }
        moduleDescriptor.setLastModified(System.currentTimeMillis());
        DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(moduleDescriptor, mrid,
                true, changing, transitive);
        for (String conf : confs) {
            dd.addDependencyConfiguration(conf, conf);
        }
        moduleDescriptor.addDependency(dd);

        return moduleDescriptor;
    }

    public static DefaultModuleDescriptor newCallerInstance(ModuleRevisionId[] mrids,
            boolean transitive, boolean changing) {
        DefaultModuleDescriptor moduleDescriptor = new DefaultModuleDescriptor(
                ModuleRevisionId.newInstance("caller", "all-caller", "working"), "integration",
                null, true);
        moduleDescriptor.addConfiguration(new Configuration(DEFAULT_CONFIGURATION));
        moduleDescriptor.setLastModified(System.currentTimeMillis());
        for (ModuleRevisionId mrid : mrids) {
            DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(moduleDescriptor,
                    mrid, true, changing, transitive);
            dd.addDependencyConfiguration(DEFAULT_CONFIGURATION, "*");
            moduleDescriptor.addDependency(dd);
        }

        return moduleDescriptor;
    }

    public static DefaultModuleDescriptor newDefaultInstance(ModuleRevisionId mrid,
            DependencyArtifactDescriptor[] artifacts) {
        DefaultModuleDescriptor moduleDescriptor = new DefaultModuleDescriptor(mrid, "release",
                null, true);
        moduleDescriptor.addConfiguration(new Configuration(DEFAULT_CONFIGURATION));
        if (artifacts != null && artifacts.length > 0) {
            for (DependencyArtifactDescriptor artifact : artifacts) {
                moduleDescriptor.addArtifact(DEFAULT_CONFIGURATION,
                    new MDArtifact(moduleDescriptor, artifact.getName(), artifact.getType(),
                            artifact.getExt(), artifact.getUrl(), artifact.getQualifiedExtraAttributes()));
            }
        } else {
            moduleDescriptor.addArtifact(DEFAULT_CONFIGURATION,
                new MDArtifact(moduleDescriptor, mrid.getName(), "jar", "jar"));
        }
        moduleDescriptor.setLastModified(System.currentTimeMillis());
        return moduleDescriptor;
    }

    public static DefaultModuleDescriptor newBasicInstance(ModuleRevisionId mrid,
            Date publicationDate) {
        DefaultModuleDescriptor moduleDescriptor = new DefaultModuleDescriptor(mrid, "release",
                publicationDate, false);
        moduleDescriptor.addConfiguration(new Configuration(DEFAULT_CONFIGURATION));
        moduleDescriptor.addArtifact(DEFAULT_CONFIGURATION,
            new MDArtifact(moduleDescriptor, mrid.getName(), "jar", "jar"));
        return moduleDescriptor;
    }

    /**
     * Transforms the given module descriptor of the given namespace and return a new module
     * descriptor in the system namespace. <i>Note that dependency exclude rules are not converted
     * in system namespace, because they aren't transformable (the namespace lacks the ability to
     * convert regular expressions)</i>
     *
     * @param md ModuleDescriptor
     * @param ns Namespace
     * @return ModuleDescriptor
     */
    public static ModuleDescriptor transformInstance(ModuleDescriptor md, Namespace ns) {
        NamespaceTransformer t = ns.getToSystemTransformer();
        if (t.isIdentity()) {
            return md;
        }
        DefaultModuleDescriptor nmd = new DefaultModuleDescriptor(md.getParser(), md.getResource());
        nmd.revId = t.transform(md.getModuleRevisionId());
        nmd.resolvedRevId = t.transform(md.getResolvedModuleRevisionId());
        nmd.status = md.getStatus();
        nmd.publicationDate = md.getPublicationDate();
        nmd.resolvedPublicationDate = md.getResolvedPublicationDate();

        for (ExtendsDescriptor ed : md.getInheritedDescriptors()) {
            ModuleDescriptor parentMd = ed.getParentMd();
            DefaultModuleDescriptor parentNmd = new DefaultModuleDescriptor(parentMd.getParser(),
                    parentMd.getResource());
            parentNmd.revId = t.transform(parentMd.getModuleRevisionId());
            parentNmd.resolvedRevId = t.transform(parentMd.getResolvedModuleRevisionId());
            parentNmd.status = parentMd.getStatus();
            parentNmd.publicationDate = parentMd.getPublicationDate();
            parentNmd.resolvedPublicationDate = parentMd.getResolvedPublicationDate();

            nmd.inheritedDescriptors.add(new DefaultExtendsDescriptor(parentNmd, ed.getLocation(),
                    ed.getExtendsTypes()));
        }

        for (DependencyDescriptor dd : md.getDependencies()) {
            nmd.dependencies.add(NameSpaceHelper.toSystem(dd, ns));
        }

        for (Configuration conf : md.getConfigurations()) {
            final String confName = conf.getName();
            nmd.configurations.put(confName, conf);
            for (Artifact art : md.getArtifacts(confName)) {
                nmd.addArtifact(confName, NameSpaceHelper.transform(art, t));
            }
        }
        nmd.setDefault(md.isDefault());
        if (md instanceof DefaultModuleDescriptor) {
            DefaultModuleDescriptor dmd = (DefaultModuleDescriptor) md;
            nmd.conflictManagers = dmd.conflictManagers.clone();
            nmd.dependencyDescriptorMediators = dmd.dependencyDescriptorMediators.clone();
        } else {
            Message.warn("transformed module descriptor is not a default module descriptor: "
                    + "impossible to copy conflict manager and version mediation configuration: "
                    + md);
        }
        nmd.licenses.addAll(Arrays.asList(md.getLicenses()));
        nmd.homePage = md.getHomePage();
        nmd.description = md.getDescription();
        nmd.lastModified = md.getLastModified();
        nmd.extraAttributesNamespaces = md.getExtraAttributesNamespaces();
        nmd.extraInfos = md.getExtraInfos();
        nmd.namespace = ns;

        return nmd;
    }

    private ModuleRevisionId revId;

    private ModuleRevisionId resolvedRevId;

    private String status = StatusManager.getCurrent().getDefaultStatus();

    private Date publicationDate;

    private Date resolvedPublicationDate;

    private List<DependencyDescriptor> dependencies = new ArrayList<>();

    private Map<String, Configuration> configurations = new LinkedHashMap<>();

    private Map<String, Collection<Artifact>> artifactsByConf = new HashMap<>();

    private Collection<Artifact> artifacts = new LinkedHashSet<>();

    // all artifacts could also be found in the artifactsByConf map, but here we can
    // preserve the order

    private boolean isDefault = false;

    private ModuleRules<ConflictManager> conflictManagers = new ModuleRules<>();

    private ModuleRules<DependencyDescriptorMediator> dependencyDescriptorMediators = new ModuleRules<>();

    private List<License> licenses = new ArrayList<>();

    private String homePage;

    private String description = "";

    private long lastModified = 0;

    private Namespace namespace;

    private String defaultConf;

    private String defaultConfMapping;

    private boolean mappingOverride;

    private ModuleDescriptorParser parser;

    private Resource resource;

    private List<ExcludeRule> excludeRules = new ArrayList<>();

    private Artifact metadataArtifact;

    private List<ExtendsDescriptor> inheritedDescriptors = new ArrayList<>();

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

    private List<ExtraInfoHolder> extraInfos = new ArrayList<>();

    public DefaultModuleDescriptor(ModuleRevisionId id, String status, Date pubDate) {
        this(id, status, pubDate, false);
    }

    public DefaultModuleDescriptor(ModuleRevisionId id, String status, Date pubDate,
            boolean isDefault) {
        if (id == null) {
            throw new NullPointerException("null module revision id not allowed");
        }
        if (status == null) {
            throw new NullPointerException("null status not allowed");
        }
        this.revId = id;
        this.resolvedRevId = id;
        this.status = status;
        this.publicationDate = pubDate;
        this.resolvedPublicationDate = publicationDate == null ? new Date() : publicationDate;
        this.isDefault = isDefault;
        this.parser = XmlModuleDescriptorParser.getInstance();
    }

    /**
     * IMPORTANT : at least call setModuleRevisionId and setResolvedPublicationDate with instances
     * created by this constructor !
     *
     * @param parser ModuleDescriptorParser
     * @param res Resource
     */
    public DefaultModuleDescriptor(ModuleDescriptorParser parser, Resource res) {
        this.parser = parser;
        resource = res;
    }

    public Artifact getMetadataArtifact() {
        if (metadataArtifact == null) {
            metadataArtifact = DefaultArtifact.newIvyArtifact(resolvedRevId,
                resolvedPublicationDate);
        }
        return metadataArtifact;
    }

    public void setModuleArtifact(Artifact moduleArtifact) {
        this.metadataArtifact = moduleArtifact;
    }

    public boolean isDefault() {
        return isDefault;
    }

    public void setPublicationDate(Date publicationDate) {
        this.publicationDate = publicationDate;
        if (resolvedPublicationDate == null) {
            resolvedPublicationDate = publicationDate == null ? new Date() : publicationDate;
        }
    }

    public Date getPublicationDate() {
        return publicationDate;
    }

    public void setResolvedPublicationDate(Date publicationDate) {
        if (publicationDate == null) {
            throw new NullPointerException("null publication date not allowed");
        }
        resolvedPublicationDate = publicationDate;
    }

    public Date getResolvedPublicationDate() {
        return resolvedPublicationDate;
    }

    public String getRevision() {
        return getResolvedModuleRevisionId().getRevision();
    }

    public void setModuleRevisionId(ModuleRevisionId revId) {
        if (revId == null) {
            throw new NullPointerException("null module revision id not allowed");
        }
        this.revId = revId;
        if (resolvedRevId == null) {
            resolvedRevId = revId;
        }
    }

    public void setResolvedModuleRevisionId(ModuleRevisionId revId) {
        resolvedRevId = revId;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public void addInheritedDescriptor(ExtendsDescriptor descriptor) {
        inheritedDescriptors.add(descriptor);
    }

    public void addDependency(DependencyDescriptor dependency) {
        dependencies.add(dependency);
    }

    public void addConfiguration(Configuration conf) {
        configurations.put(conf.getName(), conf);
    }

    /**
     * Artifact configurations are not used since added artifact may not be entirely completed, so
     * its configurations data may not be accurate
     *
     * @param conf ditto
     * @param artifact ditto
     */
    public void addArtifact(String conf, Artifact artifact) {
        Configuration c = getConfiguration(conf);
        if (c == null) {
            throw new IllegalArgumentException("Cannot add artifact '"
                    + artifact.getId().getArtifactId().getShortDescription()
                    + "' to configuration '" + conf + "' of module " + revId
                    + " because this configuration doesn't exist!");
        }
        if (c instanceof ConfigurationGroup) {
            ConfigurationGroup group = (ConfigurationGroup) c;
            for (String member : group.getMembersConfigurationNames()) {
                addArtifact(member, artifact);
            }
        } else {
            Collection<Artifact> artifacts = artifactsByConf.get(conf);
            if (artifacts == null) {
                artifacts = new ArrayList<>();
                artifactsByConf.put(conf, artifacts);
            }
            artifacts.add(artifact);
            this.artifacts.add(artifact);
        }
    }

    public ModuleRevisionId getModuleRevisionId() {
        return revId;
    }

    public ModuleRevisionId getResolvedModuleRevisionId() {
        return resolvedRevId;
    }

    public String getStatus() {
        return status;
    }

    public ExtendsDescriptor[] getInheritedDescriptors() {
        return inheritedDescriptors.toArray(new ExtendsDescriptor[inheritedDescriptors.size()]);
    }

    public Configuration[] getConfigurations() {
        return configurations.values().toArray(new Configuration[configurations.size()]);
    }

    public String[] getConfigurationsNames() {
        return configurations.keySet().toArray(new String[configurations.size()]);
    }

    public String[] getPublicConfigurationsNames() {
        List<String> ret = new ArrayList<>();
        for (Configuration conf : configurations.values()) {
            if (PUBLIC.equals(conf.getVisibility())) {
                ret.add(conf.getName());
            }
        }
        return ret.toArray(new String[ret.size()]);
    }

    /**
     * @param confName String
     * @return the configuration object with the given name in the current module descriptor, null
     * if not found.
     */
    public Configuration getConfiguration(String confName) {
        Configuration configuration = configurations.get(confName);
        if (configuration == null && confName != null) {
            // let's first check if the configuration is a conf group
            Matcher m = Pattern.compile("\\*\\[([^=]+)\\=([^\\]]+)\\]").matcher(confName);
            if (m.matches()) {
                String attName = m.group(1);
                String attValue = m.group(2);

                // this is a conf group, let's search for its members
                Map<String, Configuration> members = new LinkedHashMap<>();
                for (Configuration conf : configurations.values()) {
                    if (attValue.equals(conf.getAttribute(attName))) {
                        members.put(conf.getName(), conf);
                    }
                }
                return new ConfigurationGroup(confName, members);
            }

            // let's see if a configuration intersection is requested
            String[] confs = confName.split("\\+");
            if (confs.length <= 1) {
                return null;
            }
            Map<String, Configuration> intersectedConfs = new LinkedHashMap<>();
            for (String conf : confs) {
                Configuration c = configurations.get(conf);
                if (c == null) {
                    Message.verbose("missing configuration '" + conf + "' from intersection "
                            + confName + " in " + this);
                    return null;
                }
                intersectedConfs.put(conf, c);
            }
            return new ConfigurationIntersection(confName, intersectedConfs);
        }
        return configuration;
    }

    public Artifact[] getArtifacts(String conf) {
        Configuration c = getConfiguration(conf);
        if (c == null) {
            return new Artifact[0];
        }
        Collection<Artifact> artifacts = artifactsByConf.get(conf);
        if (c instanceof ConfigurationIntersection) {
            ConfigurationIntersection intersection = (ConfigurationIntersection) c;
            Set<Artifact> intersectedArtifacts = new LinkedHashSet<>();
            for (String intersect : intersection.getIntersectedConfigurationNames()) {
                Collection<Artifact> arts = getArtifactsIncludingExtending(intersect);
                if (intersectedArtifacts.isEmpty()) {
                    intersectedArtifacts.addAll(arts);
                } else {
                    intersectedArtifacts.retainAll(arts);
                }
            }
            if (artifacts != null) {
                intersectedArtifacts.addAll(artifacts);
            }
            return intersectedArtifacts.toArray(new Artifact[intersectedArtifacts.size()]);
        } else if (c instanceof ConfigurationGroup) {
            ConfigurationGroup group = (ConfigurationGroup) c;
            Set<Artifact> groupArtifacts = new LinkedHashSet<>();
            for (String member : group.getMembersConfigurationNames()) {
                groupArtifacts.addAll(getArtifactsIncludingExtending(member));
            }
            if (artifacts != null) {
                groupArtifacts.addAll(artifacts);
            }
            return groupArtifacts.toArray(new Artifact[groupArtifacts.size()]);
        } else {
            if (artifacts == null) {
                return new Artifact[0];
            } else {
                return artifacts.toArray(new Artifact[artifacts.size()]);
            }
        }
    }

    private Collection<Artifact> getArtifactsIncludingExtending(String conf) {
        Set<Artifact> artifacts = new LinkedHashSet<>();
        Collection<Artifact> arts = artifactsByConf.get(conf);
        if (arts != null) {
            artifacts.addAll(arts);
        }
        for (Configuration extendingConf : findConfigurationExtending(conf, getConfigurations())) {
            arts = artifactsByConf.get(extendingConf.getName());
            if (arts != null) {
                artifacts.addAll(arts);
            }
        }
        return artifacts;
    }

    public Artifact[] getAllArtifacts() {
        return artifacts.toArray(new Artifact[artifacts.size()]);
    }

    public DependencyDescriptor[] getDependencies() {
        return dependencies.toArray(new DependencyDescriptor[dependencies.size()]);
    }

    public boolean dependsOn(VersionMatcher matcher, ModuleDescriptor md) {
        for (DependencyDescriptor dd : dependencies) {
            if (dd.getDependencyId().equals(md.getModuleRevisionId().getModuleId())) {
                if (md.getResolvedModuleRevisionId().getRevision() == null) {
                    return true;
                } else if (matcher.accept(dd.getDependencyRevisionId(), md)) {
                    return true;
                }
            }
        }
        return false;
    }

    public void toIvyFile(File destFile) throws ParseException, IOException {
        if (parser != null && resource != null) {
            parser.toIvyFile(resource.openStream(), resource, destFile, this);
        } else {
            XmlModuleDescriptorWriter.write(this, destFile);
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((revId == null) ? 0 : revId.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof DefaultModuleDescriptor)) {
            return false;
        }
        DefaultModuleDescriptor other = (DefaultModuleDescriptor) obj;
        return revId == null ? other.revId == null : revId.equals(other.revId);
    }

    @Override
    public String toString() {
        return "module: " + revId + " status=" + status + " publication=" + publicationDate
                + " configurations=" + configurations + " artifacts=" + artifactsByConf
                + " dependencies=" + dependencies;
    }

    public void setDefault(boolean b) {
        isDefault = b;
    }

    /**
     * regular expressions as explained in Pattern class may be used in ModuleId organisation and
     * name
     *
     * @param moduleId ditto
     * @param matcher PatternMatcher
     * @param manager ConflictManager
     */
    public void addConflictManager(ModuleId moduleId, PatternMatcher matcher,
            ConflictManager manager) {
        conflictManagers.defineRule(new MapMatcher(moduleId.getAttributes(), matcher), manager);
    }

    public ConflictManager getConflictManager(ModuleId moduleId) {
        return conflictManagers.getRule(moduleId);
    }

    public void addDependencyDescriptorMediator(ModuleId moduleId, PatternMatcher matcher,
            DependencyDescriptorMediator ddm) {
        dependencyDescriptorMediators.defineRule(new MapMatcher(moduleId.getAttributes(), matcher),
            ddm);
    }

    public DependencyDescriptor mediate(DependencyDescriptor dd) {
        for (DependencyDescriptorMediator mediator : dependencyDescriptorMediators.getRules(dd.getDependencyId())) {
            dd = mediator.mediate(dd);
        }
        return dd;
    }

    public ModuleRules<DependencyDescriptorMediator> getAllDependencyDescriptorMediators() {
        return dependencyDescriptorMediators.clone();
    }

    public void addLicense(License license) {
        licenses.add(license);
    }

    public License[] getLicenses() {
        return licenses.toArray(new License[licenses.size()]);
    }

    public String getHomePage() {
        return homePage;
    }

    public void setHomePage(String homePage) {
        this.homePage = homePage;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public long getLastModified() {
        return lastModified;
    }

    public void setLastModified(long lastModified) {
        this.lastModified = lastModified;
    }

    public Namespace getNamespace() {
        return namespace;
    }

    public boolean isNamespaceUseful() {
        for (DependencyDescriptor dd : dependencies) {
            if (dd.getAllExcludeRules().length > 0) {
                return true;
            }
        }
        return false;
    }

    public void setNamespace(Namespace ns) {
        namespace = ns;
    }

    /**
     * Throws an exception if the module descriptor is inconsistent. For the moment, only extended
     * configurations existence and cycles are checked
     */
    public void check() {
        Stack<String> confs = new Stack<>();
        for (Configuration conf : configurations.values()) {
            for (String ext : conf.getExtends()) {
                confs.push(conf.getName());
                checkConf(confs, ext.trim());
                confs.pop();
            }
        }
    }

    private void checkConf(Stack<String> confs, String confName) {
        int index = confs.indexOf(confName);
        if (index != -1) {
            StringBuilder cycle = new StringBuilder();
            for (; index < confs.size(); index++) {
                cycle.append(confs.get(index)).append(" => ");
            }
            cycle.append(confName);
            throw new IllegalStateException("illegal cycle detected in configuration extension: "
                    + cycle);
        }
        Configuration conf = getConfiguration(confName);
        if (conf == null) {
            throw new IllegalStateException("unknown configuration '" + confName
                    + "'. It is extended by " + confs.get(confs.size() - 1));
        }
        for (String ext : conf.getExtends()) {
            confs.push(conf.getName());
            checkConf(confs, ext.trim());
            confs.pop();
        }
    }

    public String getDefaultConf() {
        return defaultConf;
    }

    public void setDefaultConf(String defaultConf) {
        this.defaultConf = defaultConf;
    }

    public String getDefaultConfMapping() {
        return defaultConfMapping;
    }

    public void setDefaultConfMapping(String defaultConfMapping) {
        this.defaultConfMapping = defaultConfMapping;
    }

    public void setMappingOverride(boolean override) {
        mappingOverride = override;
    }

    public boolean isMappingOverride() {
        return mappingOverride;
    }

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

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

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

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

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

    public ModuleDescriptorParser getParser() {
        return parser;
    }

    public Resource getResource() {
        return resource;
    }

    public void addExcludeRule(ExcludeRule rule) {
        excludeRules.add(rule);
    }

    public boolean canExclude() {
        return !excludeRules.isEmpty();
    }

    /**
     * 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;
    }

    public ExcludeRule[] getAllExcludeRules() {
        return excludeRules.toArray(new ExcludeRule[excludeRules.size()]);
    }

    public ExcludeRule[] getExcludeRules(String[] moduleConfigurations) {
        Set<ExcludeRule> rules = new LinkedHashSet<>();
        for (ExcludeRule rule : excludeRules) {
            String[] ruleConfs = rule.getConfigurations();
            if (containsAny(ruleConfs, moduleConfigurations)) {
                rules.add(rule);
            }
        }
        return rules.toArray(new ExcludeRule[rules.size()]);
    }

    private boolean containsAny(String[] arr1, String[] arr2) {
        return new ArrayList<>(Arrays.asList(arr1)).removeAll(Arrays.asList(arr2));
    }

    public Map<String, String> getExtraAttributesNamespaces() {
        return extraAttributesNamespaces;
    }

    public void addExtraAttributeNamespace(String prefix, String namespace) {
        extraAttributesNamespaces.put(prefix, namespace);
    }

    @Deprecated
    public void addExtraInfo(String infoKey, String value) {
        extraInfos.add(new ExtraInfoHolder(infoKey, value));
    }

    @Deprecated
    public Map<String, String> getExtraInfo() {
        Map<String, String> map = new HashMap<>();
        for (ExtraInfoHolder extraInfo : extraInfos) {
            populateExtraInfoMap(map, extraInfo);
        }
        return map;
    }

    private void populateExtraInfoMap(Map<String, String> map, ExtraInfoHolder extraInfo) {
        map.put(extraInfo.getName(), extraInfo.getContent());
        for (ExtraInfoHolder nested : extraInfo.getNestedExtraInfoHolder()) {
            populateExtraInfoMap(map, nested);
        }
    }

    public List<ExtraInfoHolder> getExtraInfos() {
        return extraInfos;
    }

    public void addExtraInfo(ExtraInfoHolder extraInfo) {
        extraInfos.add(extraInfo);
    }

    public String getExtraInfoContentByTagName(String tagName) {
        ExtraInfoHolder extraInfoByTagName = getExtraInfoByTagName(tagName);
        if (extraInfoByTagName != null) {
            return extraInfoByTagName.getContent();
        }
        return null;
    }

    public ExtraInfoHolder getExtraInfoByTagName(String tagName) {
        for (ExtraInfoHolder extraInfoHolder : extraInfos) {
            if (extraInfoHolder.getName().equals(tagName)) {
                return extraInfoHolder;
            }
        }
        return null;
    }
}
