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.retrieve;
  
  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.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.ivy.core.IvyContext;
  import org.apache.ivy.core.IvyPatternHelper;
  import org.apache.ivy.core.LogOptions;
  import org.apache.ivy.core.cache.ResolutionCacheManager;
  import org.apache.ivy.core.event.EventManager;
  import org.apache.ivy.core.event.retrieve.EndRetrieveArtifactEvent;
  import org.apache.ivy.core.event.retrieve.EndRetrieveEvent;
  import org.apache.ivy.core.event.retrieve.StartRetrieveArtifactEvent;
  import org.apache.ivy.core.event.retrieve.StartRetrieveEvent;
  import org.apache.ivy.core.module.descriptor.Artifact;
  import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
  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.pack.PackagingManager;
  import org.apache.ivy.core.report.ArtifactDownloadReport;
  import org.apache.ivy.core.resolve.ResolveOptions;
  import org.apache.ivy.plugins.report.XmlReportParser;
  import org.apache.ivy.util.FileUtil;
  import org.apache.ivy.util.Message;
  
  public class RetrieveEngine {
      private static final int KILO = 1024;
  
      private RetrieveEngineSettings settings;
  
      private EventManager eventManager;
  
      public RetrieveEngine(RetrieveEngineSettings settings, EventManager eventManager) {
          this.settings = settings;
          this.eventManager = eventManager;
      }
  
      /**
       * example of destFilePattern : - lib/[organisation]/[module]/[artifact]-[revision].[type] -
       * lib/[artifact].[type] : flatten with no revision moduleId is used with confs and
       * localCacheDirectory to determine an ivy report file, used as input for the copy If such a
       * file does not exist for any conf (resolve has not been called before ?) then an
       * IllegalStateException is thrown and nothing is copied.
       *
       * @param mrid ModuleRevisionId
       * @param destFilePattern String
       * @param options RetrieveOptions
       * @return int
       * @throws IOException if something goes wrong
       * @deprecated Use
       *             {@link #retrieve(org.apache.ivy.core.module.id.ModuleRevisionId, RetrieveOptions)}
       *             instead
       */
      @Deprecated
      public int retrieve(ModuleRevisionId mrid, String destFilePattern, RetrieveOptions options)
              throws IOException {
          RetrieveOptions retrieveOptions = new RetrieveOptions(options);
          retrieveOptions.setDestArtifactPattern(destFilePattern);
  
          RetrieveReport result = retrieve(mrid, retrieveOptions);
          return result.getNbrArtifactsCopied();
      }
  
      public RetrieveReport retrieve(ModuleRevisionId mrid, RetrieveOptions options)
              throws IOException {
          RetrieveReport report = new RetrieveReport();
  
          ModuleId moduleId = mrid.getModuleId();
          if (LogOptions.LOG_DEFAULT.equals(options.getLog())) {
              Message.info(":: retrieving :: " + moduleId + (options.isSync() ? " [sync]" : ""));
          } else {
              Message.verbose(":: retrieving :: " + moduleId + (options.isSync() ? " [sync]" : ""));
          }
          Message.verbose("\tcheckUpToDate=" + settings.isCheckUpToDate());
          long start = System.currentTimeMillis();
  
          String destFilePattern = IvyPatternHelper.substituteVariables(
              options.getDestArtifactPattern(), settings.getVariables());
          String destIvyPattern = IvyPatternHelper.substituteVariables(options.getDestIvyPattern(),
              settings.getVariables());
  
          String[] confs = getConfs(mrid, options);
          if (LogOptions.LOG_DEFAULT.equals(options.getLog())) {
              Message.info("\tconfs: " + Arrays.asList(confs));
          } else {
              Message.verbose("\tconfs: " + Arrays.asList(confs));
          }
          if (this.eventManager != null) {
              this.eventManager.fireIvyEvent(new StartRetrieveEvent(mrid, confs, options));
          }
  
          try {
              Map<ArtifactDownloadReport, Set<String>> artifactsToCopy = determineArtifactsToCopy(
                  mrid, destFilePattern, options);
              File fileRetrieveRoot = settings.resolveFile(IvyPatternHelper
                      .getTokenRoot(destFilePattern));
              report.setRetrieveRoot(fileRetrieveRoot);
              File ivyRetrieveRoot = destIvyPattern == null ? null : settings
                      .resolveFile(IvyPatternHelper.getTokenRoot(destIvyPattern));
              Collection<File> targetArtifactsStructure = new HashSet<>();
              // Set(File) set of all paths which should be present at then end of retrieve (useful
              // for sync)
              Collection<File> targetIvysStructure = new HashSet<>(); // same for ivy files
  
              // do retrieve
              long totalCopiedSize = 0;
              for (Map.Entry<ArtifactDownloadReport, Set<String>> artifactAndPaths : artifactsToCopy
                      .entrySet()) {
                  ArtifactDownloadReport artifact = artifactAndPaths.getKey();
                  File archive = artifact.getLocalFile();
                  if (artifact.getUnpackedLocalFile() != null) {
                      archive = artifact.getUnpackedLocalFile();
                  }
                  if (archive == null) {
                      Message.verbose("\tno local file available for " + artifact + ": skipping");
                      continue;
                  }
                  Message.verbose("\tretrieving " + archive);
                  for (String path : artifactAndPaths.getValue()) {
                      IvyContext.getContext().checkInterrupted();
                      File destFile = settings.resolveFile(path);
                      if (!settings.isCheckUpToDate() || !upToDate(archive, destFile, options)) {
                          Message.verbose("\t\tto " + destFile);
                          if (this.eventManager != null) {
                              this.eventManager.fireIvyEvent(new StartRetrieveArtifactEvent(artifact, destFile));
                          }
                          if (options.isMakeSymlinks()) {
                              boolean symlinkCreated;
                              try {
                                  symlinkCreated = FileUtil.symlink(archive, destFile,  true);
                              } catch (IOException ioe) {
                                  symlinkCreated = false;
                                  // warn about the inability to create a symlink
                                  Message.warn("symlink creation failed at path " + destFile, ioe);
                              }
                              if (!symlinkCreated) {
                                  // since symlink creation failed, let's attempt to an actual copy instead
                                  Message.info("Attempting a copy operation (since symlink creation failed) at path " + destFile);
                                  FileUtil.copy(archive, destFile, null, true);
                              }
                          } else {
                              FileUtil.copy(archive, destFile, null, true);
                          }
                          if (this.eventManager != null) {
                              this.eventManager.fireIvyEvent(new EndRetrieveArtifactEvent(artifact, destFile));
                          }
                          totalCopiedSize += FileUtil.getFileLength(destFile);
                          report.addCopiedFile(destFile, artifact);
                      } else {
                          Message.verbose("\t\tto " + destFile + " [NOT REQUIRED]");
                          report.addUpToDateFile(destFile, artifact);
                      }
  
                      if ("ivy".equals(artifact.getType())) {
                          targetIvysStructure
                                  .addAll(FileUtil.getPathFiles(ivyRetrieveRoot, destFile));
                      } else {
                          Collection<File> files = FileUtil.listAll(destFile,
                              Collections.<String> emptyList());
                          for (File file : files) {
                              targetArtifactsStructure.addAll(FileUtil.getPathFiles(fileRetrieveRoot,
                                  file));
                          }
                      }
                  }
              }
  
              if (options.isSync()) {
                  Message.verbose("\tsyncing...");
  
                  String[] ignorableFilenames = settings.getIgnorableFilenames();
                  Collection<String> ignoreList = Arrays.asList(ignorableFilenames);
  
                  Collection<File> existingArtifacts = FileUtil.listAll(fileRetrieveRoot, ignoreList);
                  Collection<File> existingIvys = (ivyRetrieveRoot == null) ? null : FileUtil.listAll(
                      ivyRetrieveRoot, ignoreList);
  
                  if (fileRetrieveRoot.equals(ivyRetrieveRoot)) {
                      targetArtifactsStructure.addAll(targetIvysStructure);
                      existingArtifacts.addAll(existingIvys);
                      sync(targetArtifactsStructure, existingArtifacts);
                  } else {
                      sync(targetArtifactsStructure, existingArtifacts);
                      if (existingIvys != null) {
                          sync(targetIvysStructure, existingIvys);
                      }
                  }
              }
              long elapsedTime = System.currentTimeMillis() - start;
              String msg = "\t"
                      + report.getNbrArtifactsCopied()
                      + " artifacts copied"
                      + (settings.isCheckUpToDate() ? (", " + report.getNbrArtifactsUpToDate() + " already retrieved")
                              : "") + " (" + (totalCopiedSize / KILO) + "kB/" + elapsedTime + "ms)";
              if (LogOptions.LOG_DEFAULT.equals(options.getLog())) {
                  Message.info(msg);
              } else {
                  Message.verbose(msg);
              }
              Message.verbose("\tretrieve done (" + (elapsedTime) + "ms)");
              if (this.eventManager != null) {
                  this.eventManager.fireIvyEvent(new EndRetrieveEvent(mrid, confs, elapsedTime,
                          report.getNbrArtifactsCopied(), report.getNbrArtifactsUpToDate(),
                          totalCopiedSize, options));
              }
  
              return report;
          } catch (Exception ex) {
              throw new RuntimeException("problem during retrieve of " + moduleId + ": " + ex, ex);
          }
      }
  
      private String[] getConfs(ModuleRevisionId mrid, RetrieveOptions options) throws IOException {
          String[] confs = options.getConfs();
          if (confs == null || (confs.length == 1 && "*".equals(confs[0]))) {
              try {
                  ModuleDescriptor md = getCache().getResolvedModuleDescriptor(mrid);
                  Message.verbose("no explicit confs given for retrieve, using ivy file: "
                          + md.getResource().getName());
                  confs = md.getConfigurationsNames();
                  options.setConfs(confs);
              } catch (IOException e) {
                  throw e;
              } catch (Exception e) {
                  throw new IOException(e.getMessage(), e);
              }
          }
          return confs;
      }
  
      private ResolutionCacheManager getCache() {
          return settings.getResolutionCacheManager();
      }
  
      private void sync(Collection<File> target, Collection<File> existing) {
          Collection<File> toRemove = new HashSet<>();
          for (File file : existing) {
              toRemove.add(file.getAbsoluteFile());
          }
          for (File file : target) {
              toRemove.remove(file.getAbsoluteFile());
          }
          for (File file : toRemove) {
              if (file.exists()) {
                  Message.verbose("\t\tdeleting " + file);
                  FileUtil.forceDelete(file);
              }
          }
      }
  
      public Map<ArtifactDownloadReport, Set<String>> determineArtifactsToCopy(ModuleRevisionId mrid,
              String destFilePattern, RetrieveOptions options) throws ParseException, IOException {
          ModuleId moduleId = mrid.getModuleId();
  
          if (options.getResolveId() == null) {
              options.setResolveId(ResolveOptions.getDefaultResolveId(moduleId));
          }
  
          ResolutionCacheManager cacheManager = getCache();
          String[] confs = getConfs(mrid, options);
          String destIvyPattern = IvyPatternHelper.substituteVariables(options.getDestIvyPattern(),
              settings.getVariables());
  
          File fileRetrieveRoot = settings.resolveFile(IvyPatternHelper
                  .getTokenRoot(destFilePattern));
          File ivyRetrieveRoot = destIvyPattern == null ? null : settings
                  .resolveFile(IvyPatternHelper.getTokenRoot(destIvyPattern));
  
          // find what we must retrieve where
  
          // ArtifactDownloadReport source -> Set (String copyDestAbsolutePath)
          final Map<ArtifactDownloadReport, Set<String>> artifactsToCopy = new HashMap<>();
          // String copyDestAbsolutePath -> Set (ArtifactRevisionId source)
          final Map<String, Set<ArtifactRevisionId>> conflictsMap = new HashMap<>();
          // String copyDestAbsolutePath -> Set (ArtifactDownloadReport source)
          final Map<String, Set<ArtifactDownloadReport>> conflictsReportsMap = new HashMap<>();
          // String copyDestAbsolutePath -> Set (String conf)
          final Map<String, Set<String>> conflictsConfMap = new HashMap<>();
  
          XmlReportParser parser = new XmlReportParser();
          for (final String conf : confs) {
              File report = cacheManager.getConfigurationResolveReportInCache(options.getResolveId(),
                  conf);
              parser.parse(report);
  
              Collection<ArtifactDownloadReport> artifacts = new ArrayList<>(
                      Arrays.asList(parser.getArtifactReports()));
              if (destIvyPattern != null) {
                  for (ModuleRevisionId rmrid : parser.getRealDependencyRevisionIds()) {
                      artifacts.add(parser.getMetadataArtifactReport(rmrid));
                  }
              }
              final PackagingManager packagingManager = new PackagingManager();
              packagingManager.setSettings(IvyContext.getContext().getSettings());
  
              for (final ArtifactDownloadReport adr : artifacts) {
  
                  final Artifact artifact = adr.getArtifact();
                  final String ext;
                  if (adr.getUnpackedLocalFile() == null) {
                      ext = artifact.getExt();
                  } else {
                      final Artifact unpackedArtifact;
                      // check if the download report is aware of the unpacked artifact
                      if (adr.getUnpackedArtifact() != null) {
                          unpackedArtifact = adr.getUnpackedArtifact();
                      } else {
                          // use the packaging manager to get hold of the unpacked artifact
                          unpackedArtifact = packagingManager.getUnpackedArtifact(artifact);
                      }
                      if (unpackedArtifact == null) {
                          throw new RuntimeException("Could not determine unpacked artifact for " + artifact
                                  + " while determining artifacts to copy for module " + mrid);
                      }
                      ext = unpackedArtifact.getExt();
                  }
  
                  String destPattern = "ivy".equals(adr.getType()) ? destIvyPattern : destFilePattern;
                  File root = "ivy".equals(adr.getType()) ? ivyRetrieveRoot : fileRetrieveRoot;
  
                  if (!"ivy".equals(adr.getType())
                          && !options.getArtifactFilter().accept(adr.getArtifact())) {
                      continue; // skip this artifact, the filter didn't accept it!
                  }
  
                  ModuleRevisionId aMrid = artifact.getModuleRevisionId();
                  String destFileName = IvyPatternHelper.substitute(destPattern,
                      aMrid.getOrganisation(), aMrid.getName(), aMrid.getBranch(),
                      aMrid.getRevision(), artifact.getName(), artifact.getType(), ext, conf,
                      adr.getArtifactOrigin(), aMrid.getQualifiedExtraAttributes(),
                      artifact.getQualifiedExtraAttributes());
                  Set<String> dest = artifactsToCopy.get(adr);
                  if (dest == null) {
                      dest = new HashSet<>();
                      artifactsToCopy.put(adr, dest);
                  }
                  File copyDestFile = settings.resolveFile(destFileName).getAbsoluteFile();
                  if (root != null &&
                      !FileUtil.isLeadingPath(root, copyDestFile)) {
                      Message.warn("not retrieving artifact " + artifact + " as its destination "
                                   + copyDestFile + " is not inside " + root);
                      continue;
                  }
                  String copyDest = copyDestFile.getPath();
  
                  String[] destinations = new String[] {copyDest};
                  if (options.getMapper() != null) {
                      destinations = options.getMapper().mapFileName(copyDest);
                  }
  
                  for (String destination : destinations) {
                      dest.add(destination);
  
                      Set<ArtifactRevisionId> conflicts = conflictsMap.get(destination);
                      Set<ArtifactDownloadReport> conflictsReports = conflictsReportsMap
                              .get(destination);
                      Set<String> conflictsConf = conflictsConfMap.get(destination);
                      if (conflicts == null) {
                          conflicts = new HashSet<>();
                          conflictsMap.put(destination, conflicts);
                      }
                      if (conflictsReports == null) {
                          conflictsReports = new HashSet<>();
                          conflictsReportsMap.put(destination, conflictsReports);
                      }
                      if (conflictsConf == null) {
                          conflictsConf = new HashSet<>();
                          conflictsConfMap.put(destination, conflictsConf);
                      }
                      if (conflicts.add(artifact.getId())) {
                          conflictsReports.add(adr);
                          conflictsConf.add(conf);
                      }
                  }
              }
          }
  
          // resolve conflicts if any
          for (Map.Entry<String, Set<ArtifactRevisionId>> entry : conflictsMap.entrySet()) {
              String copyDest = entry.getKey();
              Set<ArtifactRevisionId> artifacts = entry.getValue();
              Set<String> conflictsConfs = conflictsConfMap.get(copyDest);
              if (artifacts.size() > 1) {
                  List<ArtifactDownloadReport> artifactsList = new ArrayList<>(
                          conflictsReportsMap.get(copyDest));
                  // conflicts battle is resolved by a sort using a conflict resolving policy
                  // comparator which consider as greater a winning artifact
                  Collections.sort(artifactsList, getConflictResolvingPolicy());
  
                  // after the sort, the winning artifact is the greatest one, i.e. the last one
                  // we fail if different artifacts of the same module are mapped to the same file
                  ArtifactDownloadReport winner = artifactsList.get(artifactsList.size() - 1);
                  ModuleRevisionId winnerMD = winner.getArtifact().getModuleRevisionId();
                  for (int i = artifactsList.size() - 2; i >= 0; i--) {
                      ArtifactDownloadReport current = artifactsList.get(i);
                      if (winnerMD.equals(current.getArtifact().getModuleRevisionId())) {
                          throw new RuntimeException("Multiple artifacts of the module " + winnerMD
                                  + " are retrieved to the same file! Update the retrieve pattern"
                                  + " to fix this error.");
                      }
                  }
  
                  Message.info("\tconflict on " + copyDest + " in " + conflictsConfs + ": "
                          + winnerMD.getRevision() + " won");
  
                  // we now iterate over the list beginning with the artifact preceding the winner,
                  // and going backward to the least artifact
                  for (int i = artifactsList.size() - 2; i >= 0; i--) {
                      ArtifactDownloadReport looser = artifactsList.get(i);
                      Message.verbose("\t\tremoving conflict looser artifact: "
                              + looser.getArtifact());
                      // for each loser, we remove the pair (loser - copyDest) in the artifactsToCopy
                      // map
                      Set<String> dest = artifactsToCopy.get(looser);
                      dest.remove(copyDest);
                      if (dest.isEmpty()) {
                          artifactsToCopy.remove(looser);
                      }
                  }
              }
          }
          return artifactsToCopy;
      }
  
      private boolean upToDate(File source, File target, RetrieveOptions options) {
          if (!target.exists()) {
              return false;
          }
  
          String overwriteMode = options.getOverwriteMode();
          if (RetrieveOptions.OVERWRITEMODE_ALWAYS.equals(overwriteMode)) {
              return false;
          }
  
          if (RetrieveOptions.OVERWRITEMODE_NEVER.equals(overwriteMode)) {
              return true;
          }
  
          if (RetrieveOptions.OVERWRITEMODE_NEWER.equals(overwriteMode)) {
              return source.lastModified() <= target.lastModified();
          }
  
          if (RetrieveOptions.OVERWRITEMODE_DIFFERENT.equals(overwriteMode)) {
              return source.lastModified() == target.lastModified();
          }
  
          // unknown, so just to be sure
          return false;
      }
  
      /**
       * The returned comparator should consider greater the artifact which gains the conflict battle.
       * This is used only during retrieve... prefer resolve conflict manager to resolve conflicts.
       *
       * @return Comparator&lt;ArtifactDownloadReport&gt;
       */
      private Comparator<ArtifactDownloadReport> getConflictResolvingPolicy() {
          return new Comparator<ArtifactDownloadReport>() {
              // younger conflict resolving policy
              public int compare(ArtifactDownloadReport o1, ArtifactDownloadReport o2) {
                  Artifact a1 = o1.getArtifact();
                  Artifact a2 = o2.getArtifact();
                  if (a1.getPublicationDate().after(a2.getPublicationDate())) {
                      // a1 is after a2 <=> a1 is younger than a2 <=> a1 wins the conflict battle
                      return +1;
                  } else if (a1.getPublicationDate().before(a2.getPublicationDate())) {
                      // a1 is before a2 <=> a2 is younger than a1 <=> a2 wins the conflict battle
                      return -1;
                  } else {
                      return 0;
                  }
              }
          };
      }
  
  }
  