content: /*
   *  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.resolve;
  
  import java.util.ArrayDeque;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.LinkedHashSet;
  import java.util.LinkedList;
  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.IvyContext;
  import org.apache.ivy.core.LogOptions;
  import org.apache.ivy.core.event.resolve.EndResolveDependencyEvent;
  import org.apache.ivy.core.event.resolve.StartResolveDependencyEvent;
  import org.apache.ivy.core.module.descriptor.Artifact;
  import org.apache.ivy.core.module.descriptor.Configuration;
  import org.apache.ivy.core.module.descriptor.DefaultArtifact;
  import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor;
  import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
  import org.apache.ivy.core.module.descriptor.IncludeRule;
  import org.apache.ivy.core.module.descriptor.MDArtifact;
  import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
  import org.apache.ivy.core.module.id.ArtifactId;
  import org.apache.ivy.core.module.id.ArtifactRevisionId;
  import org.apache.ivy.core.module.id.ModuleId;
  import org.apache.ivy.core.module.id.ModuleRevisionId;
  import org.apache.ivy.core.resolve.IvyNodeCallers.Caller;
  import org.apache.ivy.core.resolve.IvyNodeEviction.EvictionData;
  import org.apache.ivy.plugins.conflict.ConflictManager;
  import org.apache.ivy.plugins.conflict.LatestCompatibleConflictManager;
  import org.apache.ivy.plugins.matcher.MatcherHelper;
  import org.apache.ivy.plugins.resolver.DependencyResolver;
  import org.apache.ivy.util.Message;
  import org.apache.ivy.util.StringUtils;
  import org.apache.ivy.util.filter.Filter;
  import org.apache.ivy.util.filter.FilterHelper;
  
  import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PRIVATE;
  import static org.apache.ivy.core.module.descriptor.Configuration.Visibility.PUBLIC;
  import static org.apache.ivy.util.StringUtils.splitToArray;
  
  public class IvyNode implements Comparable<IvyNode> {
      private static final Pattern FALLBACK_CONF_PATTERN = Pattern.compile("(.+)\\((.*)\\)");
  
      // //////// CONTEXT
      private ResolveData data;
  
      private ResolveEngineSettings settings;
  
      // //////// DELEGATES
      private IvyNodeCallers callers;
  
      private IvyNodeEviction eviction;
  
      // //////// MAIN DATA
  
      private IvyNode root;
  
      // id as requested, i.e. may be with latest rev
      private ModuleRevisionId id;
  
      // set only when node has been built or updated from a DependencyDescriptor
      private Map<IvyNode, DependencyDescriptor> dds = new HashMap<>();
  
      // Set when data has been loaded only, or when constructed from a module descriptor
      private ModuleDescriptor md;
  
      private ResolvedModuleRevision module;
  
      // //////// LOADING METADATA
      private Exception problem = null;
  
      private boolean downloaded = false;
  
      private boolean searched = false;
  
      private Collection<String> confsToFetch = new HashSet<>();
  
      private Collection<String> fetchedConfigurations = new HashSet<>();
  
      private Collection<String> loadedRootModuleConfs = new HashSet<>();
  
      // //////// USAGE DATA
  
      private IvyNodeUsage usage = new IvyNodeUsage(this);
  
      // usage information merged from evicted nodes this node is "replacing"
      private Map<ModuleRevisionId, IvyNodeUsage> mergedUsages = new LinkedHashMap<>();
  
      public IvyNode(ResolveData data, IvyNode parent, DependencyDescriptor dd) {
          id = dd.getDependencyRevisionId();
          dds.put(parent, dd);
          root = parent.getRoot();
          init(data);
      }
  
      public IvyNode(ResolveData data, ModuleDescriptor md) {
          id = md.getModuleRevisionId();
          this.md = md;
          root = this;
          init(data);
      }
  
      private void init(ResolveData data) {
          this.data = data;
          settings = data.getSettings();
          eviction = new IvyNodeEviction(this);
          callers = new IvyNodeCallers(this);
      }
  
      /**
       * After the call node may be discarded. To avoid using discarded node, make sure to get the
       * real node after the call
       * <code>IvyNode node = ... node.loadData(); node = node.getRealNode(); ...</code>
       *
       * @param rootModuleConf String
       * @param parent IvyNode
       * @param parentConf String
       * @param conf String
       * @param shouldBePublic boolean
       * @param usage IvyNodeUsage
       * @return boolean
       */
      public boolean loadData(String rootModuleConf, IvyNode parent, String parentConf, String conf,
              boolean shouldBePublic, IvyNodeUsage usage) {
          Message.debug("loadData of " + this.toString() + " of rootConf=" + rootModuleConf);
          if (!isRoot() && (data.getReport() != null)) {
              data.getReport().addDependency(this);
          }
  
          boolean loaded = false;
          if (hasProblem()) {
              Message.debug("Node has problem.  Skip loading");
          } else if (isEvicted(rootModuleConf)) {
              Message.debug(rootModuleConf + " is evicted.  Skip loading");
          } else if (!hasConfigurationsToLoad() && isRootModuleConfLoaded(rootModuleConf)) {
              Message.debug(rootModuleConf + " is loaded and no conf to load.  Skip loading");
          } else {
              markRootModuleConfLoaded(rootModuleConf);
              if (md == null) {
                  ResolveEngine engine = IvyContext.getContext().getIvy().getResolveEngine();
                  // get the resolver configured on the engine and if none is configured then
                  // get the one configured in ivy settings
                  DependencyResolver resolver = engine.getDictatorResolver();
                  if (resolver == null) {
                      resolver = data.getSettings().getResolver(getId());
                  }
                  if (resolver == null) {
                      Message.error("no resolver found for " + getModuleId()
                              + ": check your configuration");
                      problem = new RuntimeException("no resolver found for " + getModuleId()
                              + ": check your configuration");
                      return false;
                  }
                  try {
                      Message.debug("\tusing " + resolver + " to resolve " + getId());
                      DependencyDescriptor dependencyDescriptor = getDependencyDescriptor(parent);
                      long start = System.currentTimeMillis();
                      ModuleRevisionId requestedRevisionId = dependencyDescriptor
                              .getDependencyRevisionId();
                      data.getEventManager().fireIvyEvent(
                          new StartResolveDependencyEvent(resolver, dependencyDescriptor,
                                  requestedRevisionId));
                      module = resolver.getDependency(dependencyDescriptor, data);
                      data.getEventManager().fireIvyEvent(
                          new EndResolveDependencyEvent(resolver, dependencyDescriptor,
                                  requestedRevisionId, module, System.currentTimeMillis() - start));
  
                      if (module != null) {
                          module.getResolver()
                                  .getRepositoryCacheManager()
                                  .saveResolvers(module.getDescriptor(),
                                      module.getResolver().getName(),
                                      module.getArtifactResolver().getName());
                          if (settings.logModuleWhenFound()
                                  && LogOptions.LOG_DEFAULT.equals(getData().getOptions().getLog())) {
                              Message.info("\tfound " + module.getId() + " in "
                                      + module.getResolver().getName());
                          } else {
                              Message.verbose("\tfound " + module.getId() + " in "
                                      + module.getResolver().getName());
                          }
  
                          // IVY-56: check if revision has actually been resolved
                          if (settings.getVersionMatcher().isDynamic(getId())
                                  && settings.getVersionMatcher().isDynamic(module.getId())) {
                              Message.error("impossible to resolve dynamic revision for " + getId()
                                      + ": check your configuration and make sure revision is part of your pattern");
                              problem = new RuntimeException("impossible to resolve dynamic revision");
                              return false;
                          }
                          if (!getId().equals(module.getId())) {
                              IvyNode resolved = data.getNode(module.getId());
                              if (resolved != null) {
                                  // found revision has already been resolved
                                  // => update it and discard this node
                                  md = module.getDescriptor(); // needed for handleConfiguration
                                  if (!handleConfiguration(loaded, rootModuleConf, parent,
                                      parentConf, conf, shouldBePublic, usage)) {
                                      return false;
                                  }
  
                                  moveToRealNode(rootModuleConf, parent, parentConf, conf,
                                      shouldBePublic, resolved);
  
                                  return true;
                              }
                              String log = "\t[" + module.getId().getRevision() + "] " + getId();
                              if (!settings.getVersionMatcher().isDynamic(getId())) {
                                  log += " (forced)";
                              }
                              if (settings.logResolvedRevision()
                                      && LogOptions.LOG_DEFAULT.equals(getData().getOptions()
                                              .getLog())) {
                                  Message.info(log);
                              } else {
                                  Message.verbose(log);
                              }
                          }
                          downloaded = module.getReport().isDownloaded();
                          searched = module.getReport().isSearched();
                          loaded = true;
  
                          md = module.getDescriptor();
                          confsToFetch.remove("*");
                          updateConfsToFetch(Arrays
                                  .asList(resolveSpecialConfigurations(getRequiredConfigurations(
                                      parent, parentConf))));
                      } else {
                          Message.warn("\tmodule not found: " + getId());
                          resolver.reportFailure();
                          problem = new RuntimeException("not found");
                          return false;
                      }
                  } catch (ResolveProcessException e) {
                      throw e;
                  } catch (Exception e) {
                      problem = e;
                      Message.debug("Unexpected error: " + problem.getMessage(), problem);
                      return false;
                  }
              } else {
                  loaded = true;
              }
          }
          handleConfiguration(loaded, rootModuleConf, parent, parentConf, conf, shouldBePublic, usage);
          if (hasProblem()) {
              Message.debug("problem : " + problem.getMessage());
              return false;
          }
          DependencyDescriptor dd = getDependencyDescriptor(parent);
          if (dd != null) {
              usage.addUsage(rootModuleConf, dd, parentConf);
          }
          return loaded;
      }
  
      private void moveToRealNode(String rootModuleConf, IvyNode parent, String parentConf,
              String conf, boolean shouldBePublic, IvyNode resolved) {
          if (resolved.md == null) {
              resolved.md = md;
          }
          if (resolved.module == null) {
              resolved.module = module;
          }
          resolved.downloaded |= module.getReport().isDownloaded();
          resolved.searched |= module.getReport().isSearched();
          resolved.dds.putAll(dds);
          resolved.updateDataFrom(this, rootModuleConf, true);
          resolved.loadData(rootModuleConf, parent, parentConf, conf, shouldBePublic, usage);
          resolved.usage.updateDataFrom(getAllUsages(), rootModuleConf);
          usage = resolved.usage;
  
          data.replaceNode(getId(), resolved, rootModuleConf); // this actually discards the node
  
          if (settings.logResolvedRevision()
                  && LogOptions.LOG_DEFAULT.equals(getData().getOptions().getLog())) {
              Message.info("\t[" + module.getId().getRevision() + "] " + getId());
          } else {
              Message.verbose("\t[" + module.getId().getRevision() + "] " + getId());
          }
      }
  
      public Collection<IvyNode> getDependencies(String rootModuleConf, String[] confs,
              String requestedConf) {
          if (md == null) {
              throw new IllegalStateException(
                      "impossible to get dependencies when data has not been loaded");
          }
          if (Arrays.asList(confs).contains("*")) {
              if (isRoot()) {
                  confs = md.getConfigurationsNames();
              } else {
                  confs = md.getPublicConfigurationsNames();
              }
          }
          Collection<IvyNode> deps = new HashSet<>();
          for (String conf : confs) {
              deps.addAll(getDependencies(rootModuleConf, conf, requestedConf));
          }
          return deps;
      }
  
      /**
       * Load the dependencies of the current node
       * <p>
       * The resulting collection of nodes may have some configuration to load
       *
       * @param rootModuleConf
       *            the requested configuration of the root module
       * @param conf
       *            the configuration to load of this node
       * @param requestedConf
       *            the actual node conf requested, possibly extending the <code>conf</code> one.
       * @return {@link Collection} of {@link IvyNode}
       */
      public Collection<IvyNode> getDependencies(String rootModuleConf, String conf,
              String requestedConf) {
          if (md == null) {
              throw new IllegalStateException(
                      "impossible to get dependencies when data has not been loaded");
          }
          // it's important to respect order => LinkedHashMap
          Map<ModuleRevisionId, IvyNode> dependencies = new LinkedHashMap<>();
          for (DependencyDescriptor dependencyDescriptor : md.getDependencies()) {
              DependencyDescriptor dd = data.mediate(dependencyDescriptor);
              String[] dependencyConfigurations = dd.getDependencyConfigurations(conf, requestedConf);
              if (dependencyConfigurations.length == 0) {
                  // no configuration of the dependency is required for current confs :
                  // it is exactly the same as if there was no dependency at all on it
                  continue;
              }
              ModuleRevisionId requestedDependencyRevisionId = dd.getDependencyRevisionId();
              if (isDependencyModuleExcluded(dd, rootModuleConf, requestedDependencyRevisionId, conf)) {
                  // the whole module is excluded, it is considered as not being part of dependencies
                  // at all
                  Message.verbose("excluding " + dd + " in " + conf);
                  continue;
              }
  
              // check if not already loaded here
              IvyNode depNode = dependencies.get(requestedDependencyRevisionId);
              if (depNode == null) {
                  // check if not already loaded during the resolve session
                  depNode = data.getNode(requestedDependencyRevisionId);
              }
  
              if (depNode == null) {
                  depNode = new IvyNode(data, this, dd);
              } else {
                  depNode.addDependencyDescriptor(this, dd);
                  if (depNode.hasProblem()) {
                      // dependency already tried to be resolved, but unsuccessfully
                      // nothing special to do
                  }
  
              }
              String[] confsArray = depNode.resolveSpecialConfigurations(dependencyConfigurations);
              Collection<String> confs = Arrays.asList(confsArray);
              depNode.updateConfsToFetch(confs);
              depNode.addRootModuleConfigurations(depNode.usage, rootModuleConf, confsArray);
              depNode.usage.setRequiredConfs(this, conf, confs);
  
              depNode.addCaller(rootModuleConf, this, conf, requestedConf, dependencyConfigurations,
                  dd);
              dependencies.put(requestedDependencyRevisionId, depNode);
          }
          return dependencies.values();
      }
  
      private void addDependencyDescriptor(IvyNode parent, DependencyDescriptor dd) {
          dds.put(parent, dd);
      }
  
      public DependencyDescriptor getDependencyDescriptor(IvyNode parent) {
          return dds.get(parent);
      }
  
      private boolean isDependencyModuleExcluded(DependencyDescriptor dd, String rootModuleConf,
              ModuleRevisionId dependencyRevisionId, String conf) {
          Artifact a = DefaultArtifact.newIvyArtifact(dependencyRevisionId, null);
          if (isRoot()) {
              // no callers, but maybe some exclude
              Boolean exclude = doesExclude(md, rootModuleConf, new String[] {rootModuleConf}, dd, a,
                  new ArrayDeque<IvyNode>());
              return exclude != null && exclude;
          }
          return callers.doesCallersExclude(rootModuleConf, a);
      }
  
      Boolean doesExclude(ModuleDescriptor md, String rootModuleConf, String[] moduleConfs,
              DependencyDescriptor dd, Artifact artifact, Deque<IvyNode> callersStack) {
          // artifact is excluded if it match any of the exclude pattern for this dependency...
          if (directlyExcludes(md, moduleConfs, dd, artifact)) {
              return Boolean.TRUE;
          }
          // ... or if it is excluded by all its callers
          IvyNode c = getData().getNode(md.getModuleRevisionId());
          if (c != null) {
              if (callersStack.contains(c)) {
                  // a circular dependency, we cannot be conclusive here
                  return null;
              }
              return c.doesCallersExclude(rootModuleConf, artifact, callersStack);
          }
          return Boolean.FALSE;
      }
  
      public boolean directlyExcludes(ModuleDescriptor md, String[] moduleConfs,
              DependencyDescriptor dd, Artifact artifact) {
          return dd != null && dd.doesExclude(moduleConfs, artifact.getId().getArtifactId())
                  || md.doesExclude(moduleConfs, artifact.getId().getArtifactId());
      }
  
      public boolean hasConfigurationsToLoad() {
          return !confsToFetch.isEmpty();
      }
  
      private boolean markRootModuleConfLoaded(String rootModuleConf) {
          return loadedRootModuleConfs.add(rootModuleConf);
      }
  
      private boolean isRootModuleConfLoaded(String rootModuleConf) {
          return loadedRootModuleConfs.contains(rootModuleConf);
      }
  
      private boolean handleConfiguration(boolean loaded, String rootModuleConf, IvyNode parent,
              String parentConf, String conf, boolean shouldBePublic, IvyNodeUsage usage) {
          if (md != null) {
              String[] confs = getRealConfs(conf);
              addRootModuleConfigurations(usage, rootModuleConf, confs);
              for (String realConf : confs) {
                  Configuration c = md.getConfiguration(realConf);
                  if (c == null) {
                      confsToFetch.remove(conf);
                      if (isConfRequiredByMergedUsageOnly(rootModuleConf, conf)) {
                          Message.verbose("configuration required by evicted revision is not available in "
                                  + "selected revision. skipping " + conf + " in " + this);
                      } else if (!conf.equals(realConf)) {
                          problem = new RuntimeException("configuration not found in " + this + ": '"
                                  + conf + "'. Missing configuration: '" + realConf
                                  + "'. It was required from " + parent + " " + parentConf);
                      } else {
                          problem = new RuntimeException("configuration not found in " + this + ": '"
                                  + realConf + "'. It was required from " + parent + " " + parentConf);
                      }
                      return false;
                  }
                  if (shouldBePublic && !isRoot() && !PUBLIC.equals(c.getVisibility())) {
                      confsToFetch.remove(conf);
                      if (isConfRequiredByMergedUsageOnly(rootModuleConf, conf)) {
                          Message.verbose("configuration required by evicted revision is not visible in "
                                  + "selected revision. skipping " + conf + " in " + this);
                      } else {
                          problem = new RuntimeException("configuration not public in " + this
                                  + ": '" + c + "'. It was required from " + parent + " "
                                  + parentConf);
                      }
                      return false;
                  }
              }
              if (loaded) {
                  fetchedConfigurations.add(conf);
                  confsToFetch.removeAll(Arrays.asList(confs));
                  confsToFetch.remove(conf);
              }
          }
          return true;
      }
  
      private String getDefaultConf(String conf) {
          Matcher m = FALLBACK_CONF_PATTERN.matcher(conf);
          return m.matches() ? m.group(2) : conf;
      }
  
      private String getMainConf(String conf) {
          Matcher m = FALLBACK_CONF_PATTERN.matcher(conf);
          return m.matches() ? m.group(1) : null;
      }
  
      public void updateConfsToFetch(Collection<String> confs) {
          confsToFetch.addAll(confs);
          confsToFetch.removeAll(fetchedConfigurations);
      }
  
      /**
       * resolve the '*' special configurations if necessary and possible
       */
      private String[] resolveSpecialConfigurations(String[] dependencyConfigurations) {
          if (dependencyConfigurations.length == 1 && dependencyConfigurations[0].startsWith("*")
                  && isLoaded()) {
              String conf = dependencyConfigurations[0];
              if ("*".equals(conf)) {
                  return getDescriptor().getPublicConfigurationsNames();
              }
              // there are exclusions in the configuration
              List<String> exclusions = Arrays.asList(conf.substring(2).split("\\!"));
  
              List<String> ret = new ArrayList<>(Arrays.asList(getDescriptor()
                      .getPublicConfigurationsNames()));
              ret.removeAll(exclusions);
  
              return ret.toArray(new String[ret.size()]);
          }
          return dependencyConfigurations;
      }
  
      /**
       * returns the required configurations from the given node
       *
       * @param in IvyNode
       * @param inConf ditto
       * @return array of configuration names
       */
      public String[] getRequiredConfigurations(IvyNode in, String inConf) {
          Collection<String> req = new LinkedHashSet<>();
          addAllIfNotNull(req, usage.getRequiredConfigurations(in, inConf));
          for (IvyNodeUsage usage : mergedUsages.values()) {
              addAllIfNotNull(req, usage.getRequiredConfigurations(in, inConf));
          }
          return req.toArray(new String[req.size()]);
      }
  
      private <T> void addAllIfNotNull(Collection<T> into, Collection<T> col) {
          if (col != null) {
              into.addAll(col);
          }
      }
  
      /**
       * returns all the current required configurations of the node
       *
       * @return array of configuration names
       */
      public String[] getRequiredConfigurations() {
          Collection<String> required = new ArrayList<>(confsToFetch.size()
                  + fetchedConfigurations.size());
          required.addAll(fetchedConfigurations);
          required.addAll(confsToFetch);
          return required.toArray(new String[required.size()]);
      }
  
      public Configuration getConfiguration(String conf) {
          if (md == null) {
              throw new IllegalStateException(
                      "impossible to get configuration when data has not been loaded");
          }
          String defaultConf = getDefaultConf(conf);
          conf = getMainConf(conf);
          Configuration configuration = md.getConfiguration(conf);
          if (configuration == null) {
              configuration = md.getConfiguration(defaultConf);
          }
          return configuration;
      }
  
      /**
       * Returns the configurations of the dependency required in a given root module configuration.
       *
       * @param rootModuleConf String
       * @return array of configuration names
       */
      public String[] getConfigurations(String rootModuleConf) {
          Set<String> depConfs = new LinkedHashSet<>();
          addAllIfNotNull(depConfs, usage.getConfigurations(rootModuleConf));
          for (IvyNodeUsage usage : mergedUsages.values()) {
              addAllIfNotNull(depConfs, usage.getConfigurations(rootModuleConf));
          }
          return depConfs.toArray(new String[depConfs.size()]);
      }
  
      protected boolean isConfRequiredByMergedUsageOnly(String rootModuleConf, String conf) {
          Set<String> confs = usage.getConfigurations(rootModuleConf);
          return confs == null || !confs.contains(conf);
      }
  
      // This is never called. Could we remove it?
      @Deprecated
      public void discardConf(String rootModuleConf, String conf) {
          Set<String> depConfs = usage.addAndGetConfigurations(rootModuleConf);
          if (md == null) {
              depConfs.remove(conf);
          } else {
              // remove all given dependency configurations to the set + extended ones
              Configuration c = md.getConfiguration(conf);
              if (conf == null) {
                  Message.warn("unknown configuration in " + getId() + ": " + conf);
              } else {
                  // recursive remove of extended configurations
                  for (String ext : c.getExtends()) {
                      discardConf(rootModuleConf, ext);
                  }
                  depConfs.remove(c.getName());
              }
          }
      }
  
      private void addRootModuleConfigurations(IvyNodeUsage usage, String rootModuleConf,
              String[] dependencyConfs) {
          if (md != null) {
              // add all given dependency configurations to the set + extended ones
              for (String dependencyConf : dependencyConfs) {
                  Configuration conf = md.getConfiguration(dependencyConf);
                  if (conf != null) {
                      // recursive add of extended
                      addRootModuleConfigurations(usage, rootModuleConf, conf.getExtends());
                  }
              }
          }
          Collections.addAll(usage.addAndGetConfigurations(rootModuleConf), dependencyConfs);
      }
  
      /**
       * Returns the root module configurations in which this dependency is required
       *
       * @return array of configuration names
       */
      public String[] getRootModuleConfigurations() {
          Set<String> confs = getRootModuleConfigurationsSet();
          return confs.toArray(new String[confs.size()]);
      }
  
      /**
       * Returns the root module configurations in which this dependency is required
       *
       * @return {@link Set} of configuration names
       */
      public Set<String> getRootModuleConfigurationsSet() {
          Set<String> confs = new LinkedHashSet<>();
          addAllIfNotNull(confs, usage.getRootModuleConfigurations());
          for (IvyNodeUsage usage : mergedUsages.values()) {
              addAllIfNotNull(confs, usage.getRootModuleConfigurations());
          }
          return confs;
      }
  
      public String[] getConfsToFetch() {
          return confsToFetch.toArray(new String[confsToFetch.size()]);
      }
  
      public String[] getRealConfs(String conf) {
          if (md == null) {
              return new String[] {conf};
          }
          String defaultConf = getDefaultConf(conf);
          conf = getMainConf(conf);
          if (md.getConfiguration(conf) == null
                  || PRIVATE.equals(md.getConfiguration(conf).getVisibility())) {
              if ("".equals(defaultConf)) {
                  return new String[0];
              }
              conf = defaultConf;
          }
          if (conf.charAt(0) == '*') {
              return resolveSpecialConfigurations(new String[] {conf});
          }
          if (conf.contains(",")) {
              return splitToArray(conf);
          }
          return new String[] {conf};
  
      }
  
      /**
       * Finds and returns a path in callers from the given module id to the current node
       *
       * @param from
       *            the module id to start the path from
       * @return a collection representing the path, starting with the from node, followed by the list
       *         of nodes being one path to the current node, excluded
       */
      private Collection<IvyNode> findPath(ModuleId from) {
          return findPath(from, this, new LinkedList<IvyNode>());
      }
  
      private Collection<IvyNode> findPath(ModuleId from, IvyNode node, List<IvyNode> path) {
          IvyNode parent = node.getDirectCallerFor(from);
          if (parent == null) {
              throw new IllegalArgumentException("no path from " + from + " to " + getId() + " found");
          }
          if (path.contains(parent)) {
              path.add(0, parent);
              Message.verbose(
                  "circular dependency found while looking for the path for another one: was looking for "
                          + from + " as a caller of " + path.get(path.size() - 1));
              return path;
          }
          path.add(0, parent);
          if (parent.getId().getModuleId().equals(from)) {
              return path;
          }
          return findPath(from, parent, path);
      }
  
      /**
       * Update data in this node from data of the given node, for the given root module
       * configuration.
       *
       * @param node
       *            the source node from which data should be copied
       * @param rootModuleConf
       *            the root module configuration for which data should be updated
       * @param real
       *            true if the node to update from actually corresponds to the same real node
       *            (usually updated because of dynamic revision resolution), false if it's not the
       *            same real node (usually updated because of node eviction)
       */
      private void updateDataFrom(IvyNode node, String rootModuleConf, boolean real) {
          // update callers
          callers.updateFrom(node.callers, rootModuleConf, real);
  
          if (real) {
              usage.updateDataFrom(node.getAllUsages(), rootModuleConf);
          } else {
              // let's copy usage information for the given rootModuleConf, into a separate usage
              // object to keep detailed data about where usage comes from
              IvyNodeUsage mergedUsage = mergedUsages.get(node.getId());
              if (mergedUsage == null) {
                  mergedUsage = new IvyNodeUsage(node);
                  mergedUsages.put(node.getId(), mergedUsage);
              }
              mergedUsage.updateDataFrom(node.getAllUsages(), rootModuleConf);
          }
  
          // update confsToFetch
          updateConfsToFetch(node.fetchedConfigurations);
          updateConfsToFetch(node.confsToFetch);
      }
  
      private Collection<IvyNodeUsage> getAllUsages() {
          Collection<IvyNodeUsage> usages = new ArrayList<>();
          usages.add(usage);
          usages.addAll(mergedUsages.values());
          return usages;
      }
  
      /**
       * Returns all the artifacts of this dependency required in all the root module configurations
       *
       * @return array of {@link Artifact}s
       */
      public Artifact[] getAllArtifacts() {
          Set<Artifact> ret = new HashSet<>();
          for (String rootModuleConf : getRootModuleConfigurationsSet()) {
              ret.addAll(Arrays.asList(getArtifacts(rootModuleConf)));
          }
          return ret.toArray(new Artifact[ret.size()]);
      }
  
      /**
       * Returns all the artifacts of this dependency required in the root module configurations in
       * which the node is not evicted nor blacklisted
       *
       * @param artifactFilter Filter
       * @return array of {@link Artifact}s
       */
      public Artifact[] getSelectedArtifacts(Filter<Artifact> artifactFilter) {
          Collection<Artifact> ret = new HashSet<>();
          for (String rootModuleConf : getRootModuleConfigurationsSet()) {
              if (!isEvicted(rootModuleConf) && !isBlacklisted(rootModuleConf)) {
                  ret.addAll(Arrays.asList(getArtifacts(rootModuleConf)));
              }
          }
          ret = FilterHelper.filter(ret, artifactFilter);
          return ret.toArray(new Artifact[ret.size()]);
      }
  
      /**
       * Returns the artifacts of this dependency required in the configurations themselves required
       * in the given root module configuration
       *
       * @param rootModuleConf String
       * @return array of {@link Artifact}s
       */
      public Artifact[] getArtifacts(String rootModuleConf) {
          // first we look for the dependency configurations required
          // in the given root module configuration
          String[] confs = getConfigurations(rootModuleConf);
          if (confs == null || confs.length == 0) {
              // no configuration required => no artifact required
              return new Artifact[0];
          }
          if (md == null) {
              throw new IllegalStateException(
                      "impossible to get artifacts when data has not been loaded. IvyNode = "
                              + this);
          }
  
          Set<Artifact> artifacts = new HashSet<>(); // the set we fill before returning
  
          // we check if we have dependencyArtifacts includes description for this rootModuleConf
          Set<DependencyArtifactDescriptor> dependencyArtifacts = usage
                  .getDependencyArtifactsSet(rootModuleConf);
  
          if (md.isDefault() && dependencyArtifacts != null && !dependencyArtifacts.isEmpty()) {
              addArtifactsFromOwnUsage(artifacts, dependencyArtifacts);
              addArtifactsFromMergedUsage(rootModuleConf, artifacts);
          } else {
              Set<IncludeRule> includes = new LinkedHashSet<>();
              addAllIfNotNull(includes, usage.getDependencyIncludesSet(rootModuleConf));
              for (IvyNodeUsage usage : mergedUsages.values()) {
                  addAllIfNotNull(includes, usage.getDependencyIncludesSet(rootModuleConf));
              }
  
              if ((dependencyArtifacts == null || dependencyArtifacts.isEmpty())
                      && includes.isEmpty()) {
                  // no artifacts / includes: we get all artifacts as defined by the descriptor
                  for (String conf : confs) {
                      artifacts.addAll(Arrays.asList(md.getArtifacts(conf)));
                  }
              } else {
                  // we have to get only artifacts listed as "includes";
                  // first we get all artifacts as defined by the module descriptor
                  // and classify them by artifact id
                  Map<ArtifactId, Artifact> allArtifacts = new HashMap<>();
                  for (String conf : confs) {
                      for (Artifact art : md.getArtifacts(conf)) {
                          allArtifacts.put(art.getId().getArtifactId(), art);
                      }
                  }
  
                  // now we add caller defined ones
                  if (dependencyArtifacts != null) {
                      addArtifactsFromOwnUsage(artifacts, dependencyArtifacts);
                  }
                  addArtifactsFromMergedUsage(rootModuleConf, artifacts);
  
                  // and now we filter according to include rules
                  Iterator<IncludeRule> it = includes.iterator();
                  while (it.hasNext()) {
                      IncludeRule dad = it.next();
                      Collection<Artifact> arts = findArtifactsMatching(dad, allArtifacts);
                      if (arts.isEmpty()) {
                          Message.error("a required artifact is not listed by module descriptor: "
                                  + dad.getId());
                          // we remove it from required list to prevent message to be displayed more
                          // than once
                          it.remove();
                      } else {
                          Message.debug(this + " in " + rootModuleConf + ": including " + arts);
                          artifacts.addAll(arts);
                      }
                  }
              }
          }
  
          // now exclude artifacts that aren't accepted by any caller
          Iterator<Artifact> iter = artifacts.iterator();
          Set<ArtifactRevisionId> artifactRevisionsSeen = new HashSet<ArtifactRevisionId>();
          while (iter.hasNext()) {
              Artifact artifact = iter.next();
              boolean excluded = callers.doesCallersExclude(rootModuleConf, artifact);
              if (excluded) {
                  Message.debug(this + " in " + rootModuleConf + ": excluding " + artifact);
                  iter.remove();
              }
              if (!artifactRevisionsSeen.add(artifact.getId())) {
                  Message.debug(this + " in " + rootModuleConf + ": skipping duplicate " + artifact);
                  iter.remove();
              }
          }
          return artifacts.toArray(new Artifact[artifacts.size()]);
      }
  
      private void addArtifactsFromOwnUsage(Set<Artifact> artifacts,
              Set<DependencyArtifactDescriptor> dependencyArtifacts) {
          for (DependencyArtifactDescriptor dad : dependencyArtifacts) {
              artifacts.add(new MDArtifact(md, dad.getName(), dad.getType(), dad.getExt(), dad
                      .getUrl(), dad.getQualifiedExtraAttributes()));
          }
      }
  
      private void addArtifactsFromMergedUsage(String rootModuleConf, Set<Artifact> artifacts) {
          for (IvyNodeUsage usage : mergedUsages.values()) {
              Set<DependencyArtifactDescriptor> mergedDependencyArtifacts = usage
                      .getDependencyArtifactsSet(rootModuleConf);
              if (mergedDependencyArtifacts != null) {
                  for (DependencyArtifactDescriptor dad : mergedDependencyArtifacts) {
                      Map<String, String> extraAttributes = new HashMap<>(
                              dad.getQualifiedExtraAttributes());
                      MDArtifact artifact = new MDArtifact(md, dad.getName(), dad.getType(),
                              dad.getExt(), dad.getUrl(), extraAttributes);
  
                      if (!artifacts.contains(artifact)) {
                          // this is later used to know that this is a merged artifact
                          extraAttributes.put("ivy:merged", dad.getDependencyDescriptor()
                                  .getParentRevisionId() + " -> " + usage.getNode().getId());
                          artifacts.add(artifact);
                      }
                  }
              }
          }
      }
  
      private static Collection<Artifact> findArtifactsMatching(IncludeRule rule,
              Map<ArtifactId, Artifact> allArtifacts) {
          Collection<Artifact> ret = new ArrayList<>();
          for (Map.Entry<ArtifactId, Artifact> entry : allArtifacts.entrySet()) {
              if (MatcherHelper.matches(rule.getMatcher(), rule.getId(), entry.getKey())) {
                  ret.add(entry.getValue());
              }
          }
          return ret;
      }
  
      public boolean hasProblem() {
          return problem != null;
      }
  
      public Exception getProblem() {
          return problem;
      }
  
      public String getProblemMessage() {
          return StringUtils.getErrorMessage(problem);
      }
  
      public boolean isDownloaded() {
          return downloaded;
      }
  
      public boolean isSearched() {
          return searched;
      }
  
      public boolean isLoaded() {
          return md != null;
      }
  
      public boolean isFetched(String conf) {
          return fetchedConfigurations.contains(conf);
      }
  
      public IvyNode findNode(ModuleRevisionId mrid) {
          return data.getNode(mrid);
      }
  
      boolean isRoot() {
          return root == this;
      }
  
      public IvyNode getRoot() {
          return root;
      }
  
      public ConflictManager getConflictManager(ModuleId mid) {
          if (md == null) {
              throw new IllegalStateException(
                      "impossible to get conflict manager when data has not been loaded. IvyNode = "
                              + this);
          }
          ConflictManager cm = md.getConflictManager(mid);
          return cm == null ? settings.getConflictManager(mid) : cm;
      }
  
      public IvyNode getRealNode() {
          IvyNode real = data.getNode(getId());
          return real != null ? real : this;
      }
  
      public ModuleRevisionId getId() {
          return id;
      }
  
      public ModuleId getModuleId() {
          return id.getModuleId();
      }
  
      public ModuleDescriptor getDescriptor() {
          return md;
      }
  
      public ResolveData getData() {
          return data;
      }
  
      public ResolvedModuleRevision getModuleRevision() {
          return module;
      }
  
      public long getPublication() {
          if (module != null) {
              return module.getPublicationDate().getTime();
          }
          return 0;
      }
  
      /**
       * Returns the last modified timestamp of the module represented by this Node, or 0 if the last
       * modified timestamp is currently unknown (module not loaded)
       *
       * @return the last modified timestamp of the module represented by this Node
       */
      public long getLastModified() {
          if (md != null) {
              return md.getLastModified();
          }
          return 0;
      }
  
      public ModuleRevisionId getResolvedId() {
          if (md != null && md.getResolvedModuleRevisionId().getRevision() != null) {
              return md.getResolvedModuleRevisionId();
          }
          if (module != null) {
              return module.getId();
          }
          return getId();
      }
  
      /**
       * Clean data related to one root module configuration only
       */
      public void clean() {
          confsToFetch.clear();
      }
  
      // /////////////////////////////////////////////////////////////////////////////
      // CALLERS MANAGEMENT
      // /////////////////////////////////////////////////////////////////////////////
  
      boolean canExclude(String rootModuleConf) {
          for (Caller caller : getCallers(rootModuleConf)) {
              if (caller.canExclude()) {
                  return true;
              }
          }
          return false;
      }
  
      private IvyNode getDirectCallerFor(ModuleId from) {
          return callers.getDirectCallerFor(from);
      }
  
      public Caller[] getCallers(String rootModuleConf) {
          return callers.getCallers(rootModuleConf);
      }
  
      public Collection<ModuleId> getAllCallersModuleIds() {
          return callers.getAllCallersModuleIds();
      }
  
      public Caller[] getAllCallers() {
          return callers.getAllCallers();
      }
  
      public Caller[] getAllRealCallers() {
          return callers.getAllRealCallers();
      }
  
      public void addCaller(String rootModuleConf, IvyNode callerNode, String callerConf,
              String requestedConf, String[] dependencyConfs, DependencyDescriptor dd) {
          callers.addCaller(rootModuleConf, callerNode, callerConf, requestedConf, dependencyConfs,
              dd);
          boolean isCircular = callers.getAllCallersModuleIds().contains(getId().getModuleId());
          if (isCircular) {
              IvyContext.getContext().getCircularDependencyStrategy()
                      .handleCircularDependency(toMrids(findPath(getId().getModuleId()), this));
          }
      }
  
      public boolean doesCallersExclude(String rootModuleConf, Artifact artifact,
              Deque<IvyNode> callersStack) {
          return callers.doesCallersExclude(rootModuleConf, artifact, callersStack);
      }
  
      @Deprecated
      public boolean doesCallersExclude(String rootModuleConf, Artifact artifact,
              Stack<ModuleRevisionId> callersStack) {
          Deque<IvyNode> callersDeque = new ArrayDeque<>();
          for (ModuleRevisionId mrid : callersStack) {
              for (Caller caller : getCallers(rootModuleConf)) {
                  if (caller.getModuleRevisionId().equals(mrid)) {
                      callersDeque.add(data.getNode(mrid));
                  }
              }
          }
          return callers.doesCallersExclude(rootModuleConf, artifact, callersDeque);
      }
  
      private ModuleRevisionId[] toMrids(Collection<IvyNode> path, IvyNode depNode) {
          ModuleRevisionId[] ret = new ModuleRevisionId[path.size() + 1];
          int i = 0;
          for (IvyNode node : path) {
              ret[i++] = node.getId();
          }
          ret[ret.length - 1] = depNode.getId();
          return ret;
      }
  
      // /////////////////////////////////////////////////////////////////////////////
      // EVICTION MANAGEMENT
      // /////////////////////////////////////////////////////////////////////////////
  
      /**
       * A copy of the set of resolved nodes (real nodes)
       *
       * @param moduleId ditto
       * @param rootModuleConf String
       * @return Set&lt;IvyNode&gt;
       */
      public Set<IvyNode> getResolvedNodes(ModuleId moduleId, String rootModuleConf) {
          return eviction.getResolvedNodes(moduleId, rootModuleConf);
      }
  
      public Collection<ModuleRevisionId> getResolvedRevisions(ModuleId moduleId,
              String rootModuleConf) {
          return eviction.getResolvedRevisions(moduleId, rootModuleConf);
      }
  
      public void markEvicted(EvictionData evictionData) {
          eviction.markEvicted(evictionData);
          String rootModuleConf = evictionData.getRootModuleConf();
  
          // bug 105: update selected data with evicted one
          if (evictionData.getSelected() != null) {
              for (IvyNode selected : evictionData.getSelected()) {
                  selected.updateDataFrom(this, rootModuleConf, false);
              }
          }
      }
  
      public Collection<ConflictManager> getAllEvictingConflictManagers() {
          return eviction.getAllEvictingConflictManagers();
      }
  
      public Collection<IvyNode> getAllEvictingNodes() {
          return eviction.getAllEvictingNodes();
      }
  
      public Collection<String> getAllEvictingNodesDetails() {
          return eviction.getAllEvictingNodesDetails();
      }
  
      public String[] getEvictedConfs() {
          return eviction.getEvictedConfs();
      }
  
      public EvictionData getEvictedData(String rootModuleConf) {
          return eviction.getEvictedData(rootModuleConf);
      }
  
      public Collection<IvyNode> getEvictedNodes(ModuleId mid, String rootModuleConf) {
          return eviction.getEvictedNodes(mid, rootModuleConf);
      }
  
      public Collection<ModuleRevisionId> getEvictedRevisions(ModuleId mid, String rootModuleConf) {
          return eviction.getEvictedRevisions(mid, rootModuleConf);
      }
  
      public EvictionData getEvictionDataInRoot(String rootModuleConf, IvyNode ancestor) {
          return eviction.getEvictionDataInRoot(rootModuleConf, ancestor);
      }
  
      public boolean isCompletelyEvicted() {
          return eviction.isCompletelyEvicted();
      }
  
      public boolean isEvicted(String rootModuleConf) {
          return eviction.isEvicted(rootModuleConf);
      }
  
      public void markEvicted(String rootModuleConf, IvyNode node, ConflictManager conflictManager,
              Collection<IvyNode> resolved) {
          EvictionData evictionData = new EvictionData(rootModuleConf, node, conflictManager,
                  resolved);
          markEvicted(evictionData);
      }
  
      public void setEvictedNodes(ModuleId moduleId, String rootModuleConf,
              Collection<IvyNode> evicted) {
          eviction.setEvictedNodes(moduleId, rootModuleConf, evicted);
      }
  
      public void setResolvedNodes(ModuleId moduleId, String rootModuleConf,
              Collection<IvyNode> resolved) {
          eviction.setResolvedNodes(moduleId, rootModuleConf, resolved);
      }
  
      @Override
      public String toString() {
          return getResolvedId().toString();
      }
  
      @Override
      public boolean equals(Object obj) {
          if (!(obj instanceof IvyNode)) {
              return false;
          }
          IvyNode node = (IvyNode) obj;
          return node.getId().equals(getId());
      }
  
      public int compareTo(IvyNode that) {
          return this.getModuleId().compareTo(that.getModuleId());
      }
  
      @Override
      public int hashCode() {
          return getId().hashCode();
      }
  
      /**
       * Returns a collection of Nodes in conflict for which conflict has been detected but conflict
       * resolution hasn't been done yet
       *
       * @param rootModuleConf ditto
       * @param mid
       *            the module id for which pending conflicts should be found
       * @return a Collection of IvyNode in pending conflict
       */
      public Collection<IvyNode> getPendingConflicts(String rootModuleConf, ModuleId mid) {
          return eviction.getPendingConflicts(rootModuleConf, mid);
      }
  
      public void setPendingConflicts(ModuleId moduleId, String rootModuleConf,
              Collection<IvyNode> conflicts) {
          eviction.setPendingConflicts(moduleId, rootModuleConf, conflicts);
      }
  
      // /////////////////////////////////////////////////////////////////////////////
      // BLACKLISTING MANAGEMENT
      // /////////////////////////////////////////////////////////////////////////////
  
      /**
       * Blacklists the current node, so that a new resolve process won't ever consider this node as
       * available in the repository.
       * <p>
       * This is useful in combination with {@link RestartResolveProcess} for conflict manager
       * implementation which use a best effort strategy to find compatible dependency set, like
       * {@link LatestCompatibleConflictManager}
       * </p>
       *
       * @param bdata
       *            the root module configuration in which the node should be blacklisted
       */
      public void blacklist(IvyNodeBlacklist bdata) {
          if (data.getSettings().logResolvedRevision()) {
              Message.info("BLACKLISTING " + bdata);
          } else {
              Message.verbose("BLACKLISTING " + bdata);
          }
  
          Stack<IvyNode> callerStack = new Stack<>();
          callerStack.push(this);
          clearEvictionDataInAllCallers(bdata.getRootModuleConf(), callerStack);
  
          usage.blacklist(bdata);
          data.blacklist(this);
      }
  
      private void clearEvictionDataInAllCallers(String rootModuleConf, Stack<IvyNode> callerStack) {
          for (Caller caller : callerStack.peek().getCallers(rootModuleConf)) {
              IvyNode callerNode = findNode(caller.getModuleRevisionId());
              if (callerNode != null) {
                  callerNode.eviction = new IvyNodeEviction(callerNode);
                  if (!callerStack.contains(callerNode)) {
                      callerStack.push(callerNode);
                      clearEvictionDataInAllCallers(rootModuleConf, callerStack);
                      callerStack.pop();
                  }
              }
          }
      }
  
      /**
       * Indicates if this node has been blacklisted in the given root module conf.
       * <p>
       * A blacklisted node should be considered as if it doesn't even exist on the repository.
       * </p>
       *
       * @param rootModuleConf
       *            the root module conf for which we'd like to know if the node is blacklisted
       *
       * @return true if this node is blacklisted int he given root module conf, false otherwise
       * @see #blacklist(IvyNodeBlacklist)
       */
      public boolean isBlacklisted(String rootModuleConf) {
          return usage.isBlacklisted(rootModuleConf);
      }
  
      /**
       * Indicates if this node has been blacklisted in all root module configurations.
       *
       * @return true if this node is blacklisted in all root module configurations, false otherwise
       * @see #blacklist(IvyNodeBlacklist)
       */
      public boolean isCompletelyBlacklisted() {
          if (isRoot()) {
              return false;
          }
          for (String rootModuleConfiguration : getRootModuleConfigurations()) {
              if (!isBlacklisted(rootModuleConfiguration)) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Returns the blacklist data of this node in the given root module conf, or <code>null</code>
       * if this node is not blacklisted in this root module conf.
       *
       * @param rootModuleConf
       *            the root module configuration to consider
       * @return the blacklist data if any
       */
      public IvyNodeBlacklist getBlacklistData(String rootModuleConf) {
          return usage.getBlacklistData(rootModuleConf);
      }
  
      public IvyNodeUsage getMainUsage() {
          return usage;
      }
  
      /**
       * Indicates if there is any of the merged usages of this node which has a depender with
       * transitive dependency descriptor.
       * <p>
       * If at there is at least one usage from the merged usages for which there is a depender in the
       * given root module conf which has a dependency descriptor with transitive == true, then it
       * returns true. Otherwise it returns false.
       * </p>
       *
       * @param rootModuleConf
       *            the root module configuration to consider
       * @return true if there is any merged usage with transitive dd, false otherwise.
       */
      public boolean hasAnyMergedUsageWithTransitiveDependency(String rootModuleConf) {
          if (mergedUsages == null) {
              return false;
          }
          for (IvyNodeUsage usage : mergedUsages.values()) {
              if (usage.hasTransitiveDepender(rootModuleConf)) {
                  return true;
              }
          }
          return false;
      }
  
  }
  