/**
 * Copyright (c) 2018 Eclipse contributors and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 */
package org.eclipse.emf.releng;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.equinox.internal.p2.metadata.BasicVersion;
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.internal.repository.tools.AbstractApplication;
import org.eclipse.equinox.p2.internal.repository.tools.CompositeRepositoryApplication;
import org.eclipse.equinox.p2.internal.repository.tools.MirrorApplication;
import org.eclipse.equinox.p2.internal.repository.tools.RepositoryDescriptor;
import org.eclipse.equinox.p2.internal.repository.tools.SlicingOptions;
import org.eclipse.equinox.p2.metadata.IArtifactKey;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IProvidedCapability;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.metadata.VersionRange;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.ICompositeRepository;
import org.eclipse.equinox.p2.repository.IRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.spi.AbstractArtifactRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.spi.AbstractMetadataRepository;
import org.osgi.framework.Bundle;


/**
 * A utility class that uses p2's repository tools to manage the update sites for EMF's builds.
 *
 */
@SuppressWarnings("restriction")
public class UpdateSiteGenerator
{
  /**
   * The root-relative location of the builds folder.
   */
  private static final String RELATIVE_BUILDS_FOLDER = "modeling/emf/emf/builds";

  /**
   * The files and folders that comprise a simple update site.
   */
  private static final List<String> UPDATE_SITE_CONTENT = Arrays.asList(
    new String []{ "binary", "features", "plugins", "artifacts.jar", "artifacts.xml.xz", "content.jar", "content.xml.xz", "p2.index" });

  /**
   * The number of nightly builds to maintain in the nightly composite.
   */
  public static final int RETAINED_NIGHTLY_BUILDS = 7;

  /**
   * The valid values for {@code publish.build.type}.
   */
  public static final List<String> BUILD_TYPES = Arrays.asList(new String []{ "nightly", "milestone", "release" });

  /**
   * The prefix qualifier associated with the {@link #BUILD_TYPES}.
   */
  public static final List<String> BUILD_TYPE_QUALIFIERS = Arrays.asList(new String []{ "N", "S", "" });

  /**
   * The value of {@code publish.download.root.folder} with normalized path segment separators.
   */
  private static final String DOWLOAD_ROOT_FOLDER_PROPERTY = System.getProperty("publish.download.root.folder").replace('\\', '/');

  /**
   * The expected value of the {@link #DOWLOAD_ROOT_FOLDER} on the {@code build.eclipse.org} host.
   */
  public static final String DOWNLOAD_ECLIPSE_ORG_FOLDER = "/home/data/httpd/download.eclipse.org/";

  /**
   * The root folder in which the update sites for EMF's builds are managed.
   */
  public static final String DOWLOAD_ROOT_FOLDER = DOWLOAD_ROOT_FOLDER_PROPERTY == null || DOWLOAD_ROOT_FOLDER_PROPERTY.trim().length() == 0
    ? DOWNLOAD_ECLIPSE_ORG_FOLDER : (!DOWLOAD_ROOT_FOLDER_PROPERTY.endsWith("/") ? DOWLOAD_ROOT_FOLDER_PROPERTY + "/" : DOWLOAD_ROOT_FOLDER_PROPERTY);

  /**
   * The location of the builds folder with in the root folder.
   */
  public static final String BUILDS_ROOT_FOLDER = DOWLOAD_ROOT_FOLDER + RELATIVE_BUILDS_FOLDER;

  /**
   * Returns the destination location at which to promote the give type of build with the given name.
   *
   * @param buildType the {@code publish.build.type}.
   * @param name the name of the folder, i.e., the {@code publish.build.timestamp} or the {@link #getVersion(String) version}.
   * @return the destination location at which to promote the give type of build with the given name.
   *
   * @throws Exception
   */
  public String getPromoteUpdateSiteDestination(String buildType, String name) throws Exception
  {
    File destinationBuildsTypeFolder = new File(BUILDS_ROOT_FOLDER, buildType);
    return new File(destinationBuildsTypeFolder, getTargetFolder(buildType, name)).getCanonicalPath().replace('\\', '/');
  }

  /**
   * Mirrors the source repository location to the destination location.
   * This is used for nightly and milestone promotion.
   *
   * @param source the source repository.
   * @param buildType the {@code publish.build.type}.
   * @param buildTimestamp the {@code publish.build.timestamp}.
   * @return the destination location at the repository was promoted.
   * @throws Exception
   */
  public File promoteUpdateSite(final String source, final String buildType, String buildTimestamp) throws Exception
  {
    final String destination = getPromoteUpdateSiteDestination(buildType, buildTimestamp);
    return mirrorUpdateSite(source, destination, buildType);
  }

  /**
   * Mirrors the source repository location to the destination location.
   * The folder name is determined by the {@link #getVersion(String) version} of the source.
   * This is used for release promotion.
   *
   * @param source the source repository.
   * @param buildType the {@code publish.build.type}.
   * @return the destination location at the repository was promoted.
   * @throws Exception
   */
  public File mirrorUpdateSite(final String source, final String buildType) throws Exception
  {
    String version = getVersion(source);
    final String destination = getPromoteUpdateSiteDestination(buildType, version);
    return mirrorUpdateSite(source, destination, buildType);
  }

  /**
   * Mirrors the source repository to the destination repository.
   * The name of the repository is computed from the content metadata of the source and from the build type,
   * the <a href="https://wiki.eclipse.org/Equinox/p2/p2.mirrorsURL">p2.mirrorsURL</a> property configured,
   * and {@code .xml.xz} formats are produced along with a {@code p2.index}.
   *
   * @param source the source repository location.
   * @param destination the destination repository location.
   * @param buildType the {@code publish.build.type}.
   * @return the destination location at the repository was mirrored.
   * @throws Exception
   */
  private File mirrorUpdateSite(final String source, final String destination, final String buildType) throws Exception
  {
    MirrorApplication mirrorApplication = new MirrorApplication()
      {
        @Override
        protected void finalizeRepositories()
        {
          if (destinationMetadataRepository instanceof AbstractMetadataRepository)
          {
            String repositoryName = getRepositoryName(destinationMetadataRepository) + ' ' + getLabel(buildType);
            destinationMetadataRepository.setProperty(IRepository.PROP_NAME, repositoryName);

            if (destinationArtifactRepository instanceof AbstractArtifactRepository)
            {
              destinationArtifactRepository.setProperty(IRepository.PROP_NAME, repositoryName);
              if (destination.startsWith(DOWLOAD_ROOT_FOLDER))
              {
                String mirrorsURL = "http://www.eclipse.org/downloads/download.php?file=/" + destination.substring(DOWLOAD_ROOT_FOLDER.length());
                destinationArtifactRepository.setProperty("p2.mirrorsURL", mirrorsURL);
              }
            }
          }

          super.finalizeRepositories();
        }
      };

    File artifacts = new File(destination, "artifacts.xml.xz");
    if (artifacts.isFile())
    {
      artifacts.delete();
    }

    File content = new File(destination, "content.xml.xz");
    if (content.isFile())
    {
      content.delete();
    }

    File p2Index = new File(destination, "p2.index");
    if (p2Index.isFile())
    {
      p2Index.delete();
    }

    mirrorApplication.initializeFromArguments(new String []{ "-source", source, "-destination", destination, "-writeMode", "clean" });

    SlicingOptions slicingOptions = new SlicingOptions();
    slicingOptions.latestVersionOnly(true);
    mirrorApplication.setSlicingOptions(slicingOptions);

    IStatus status = mirrorApplication.run(new NullProgressMonitor());
    if (!status.isOK())
    {
      throw new CoreException(status);
    }

    xzCompress(destination);

    return new File(destination);
  }

  /**
   * Returns the version of the {@code org.eclipse.emf.sdk.feature.group} installable unit in the target repository.
   * @param targetRepository the repository location.
   * @return the associated semantic version of the repository.
   * @throws Exception
   */
  public String getVersion(String targetRepository) throws Exception
  {
    RepositoryAnalyzer repositoryAnalyzer = new RepositoryAnalyzer();
    RepositoryDescriptor repositoryDescriptor = new RepositoryDescriptor();
    repositoryDescriptor.setLocation(createURI(targetRepository));
    repositoryAnalyzer.addSource(repositoryDescriptor);
    String version = repositoryAnalyzer.getVersion();
    return version;
  }

  /**
   * Creates and returns the repository analyzer for the give repositories.
   * @param repositories a list of repository location.
   *
   * @return the repository analyzer.
   */
  public RepositoryAnalyzer getRepositoryAnalyzer(List<String> repositories)
  {
    RepositoryAnalyzer repositoryAnalyzer = new RepositoryAnalyzer();
    RepositoryDescriptor repositoryDescriptor = new RepositoryDescriptor();
    for (String repository : repositories)
    {
      repositoryDescriptor.setLocation(createURI(repository));
      repositoryAnalyzer.addSource(repositoryDescriptor);
    }

    return repositoryAnalyzer;
  }

  /**
   * Returns the title case label for the give build type.
   * @param buildType the {@code publish.build.type}.
   * o
   * @return the title case label for the give build type.
   */
  private String getLabel(String buildType)
  {
    return Character.toUpperCase(buildType.charAt(0)) + buildType.substring(1);
  }

  /**
   * Returns the computed name for the repository.
   * This will be 'EMF' followed the range of versions of the {@code org.eclipse.emf.sdk.feature.group} installable units in the repository.
   * If there is only one version then it will be followed by only that one version.
   * If there are none, then it's just 'EMF'.
   * @param repository
   * @return the computed name for the repository.
   */
  private String getRepositoryName(IMetadataRepository repository)
  {
    IQueryResult<IInstallableUnit> groups = repository.query(QueryUtil.createIUQuery("org.eclipse.emf.sdk.feature.group"), new NullProgressMonitor());
    List<BasicVersion> versions = new ArrayList<BasicVersion>();
    for (Iterator<IInstallableUnit> i = groups.iterator(); i.hasNext();)
    {
      IInstallableUnit group = i.next();
      Version iuVersion = group.getVersion();
      if (iuVersion.isOSGiCompatible() && iuVersion instanceof BasicVersion)
      {
        BasicVersion basicVersion = (BasicVersion)iuVersion;
        versions.add(basicVersion);
      }
    }

    if (versions.isEmpty())
    {
      return "EMF";
    }
    else if (versions.size() == 1)
    {
      BasicVersion version = versions.get(0);
      return "EMF " + version.getMajor() + "." + version.getMinor();
    }
    else
    {
      Collections.sort(versions);
      BasicVersion minVersion = versions.get(0);
      BasicVersion maxVersion = versions.get(versions.size() - 1);
      return "EMF " + minVersion.getMajor() + "." + minVersion.getMinor() + "-" + maxVersion.getMajor() + "." + maxVersion.getMinor();
    }
  }

  /**
   * Returns the destination folder for the given build type and whether it is a latest composite or not.
   *
   * @param buildType the {@code publish.build.type}.
   * @param latest whether this is a composite for the latest build.
   * @return the destination folder.
   *
   * @throws Exception
   */
  public String getCompositeUpdateSiteDestination(String buildType, boolean latest) throws Exception
  {
    File destinationBuildsTypeFolder = new File(BUILDS_ROOT_FOLDER, buildType);
    if (latest)
    {
      destinationBuildsTypeFolder = new File(destinationBuildsTypeFolder, "latest");
    }
    return getCanonicalPath(destinationBuildsTypeFolder);
  }

  /**
   * Creates a composite update that references the given source repositories, at a location determined by the build type and the latest indicator.
   * @param sources the source repositories.
   * @param buildType the {@code publish.build.type}.
   * @param latest whether is is a composite refering to the latest.
   *
   * @throws Exception
   */
  public String composeUpdateSites(List<String> sources, final String buildType, final boolean latest) throws Exception
  {
    String destination = getCompositeUpdateSiteDestination(buildType, latest);
    final URI destinationURI = createURI(destination);

    // We must set the user dir to ensure that we produce a composite that uses relative URIs!
    //
    System.setProperty("user.dir", new File(destination).getPath());

    CompositeRepositoryApplication compositeRepositoryApplication = new CompositeRepositoryApplication()
      {
        @Override
        protected void finalizeRepositories()
        {
          // Compute an appropriate name for the repository after it has been populated.
          if (destinationMetadataRepository instanceof ICompositeRepository<?>)
          {
            String repositoryName = getRepositoryName(destinationMetadataRepository) + ' ' + getLabel(buildType) + (latest ? " Latest" : " Composite");
            destinationMetadataRepository.setProperty(IRepository.PROP_NAME, repositoryName);
            save((ICompositeRepository<?>)destinationMetadataRepository);

            if (destinationArtifactRepository instanceof ICompositeRepository<?>)
            {
              destinationArtifactRepository.setProperty(IRepository.PROP_NAME, repositoryName);
              save((ICompositeRepository<?>)destinationArtifactRepository);
            }
          }

          super.finalizeRepositories();
        }

        private void save(ICompositeRepository<?> repository)
        {
          // Unfortunately p2 provides no API for saving the repository after changing the name.
          // The repository is saved only when children are added or removed.
          // So this a good opportunity to double check that all the children are usin a relative URI.
          List<URI> children = repository.getChildren();
          repository.removeAllChildren();
          for (URI child : children)
          {
            URI relativeSourceURI = relativize(child, destinationURI);
            if (relativeSourceURI == child)
            {
              throw new IllegalArgumentException("The URI '" + child + "' cannot be made relative to '" + destinationURI + "'");
            }
            repository.addChild(relativeSourceURI);
          }
        }
      };
    compositeRepositoryApplication.setRemoveAll(true);

    RepositoryDescriptor destinationRepositoryDescriptor = new RepositoryDescriptor();
    destinationRepositoryDescriptor.setLocation(destinationURI);
    compositeRepositoryApplication.addDestination(destinationRepositoryDescriptor);

    for (String source : sources)
    {
      RepositoryDescriptor childRepositoryDescriptor = new RepositoryDescriptor();
      URI sourceURI = createURI(source);
      URI relativeSourceURI = relativize(sourceURI, destinationURI);
      if (relativeSourceURI == sourceURI)
      {
        // We must use relative URIs!
        throw new IllegalArgumentException("The URI '" + sourceURI + "' cannot be made relative to '" + destinationURI + "'");
      }

      childRepositoryDescriptor.setLocation(relativeSourceURI);
      compositeRepositoryApplication.addChild(childRepositoryDescriptor);
    }

    IStatus status = compositeRepositoryApplication.run(new NullProgressMonitor());
    if (!status.isOK())
    {
      throw new CoreException(status);
    }

    return destination;
  }

  /**
   * Returns the relative URI that the target can use to reference the source.
   * It would be better to use EMF's URI.
   *
   * @param sourceURI the source URI.
   * @param targetURI the target URI.
   * @return the relative URI.
   */
  private static URI relativize(URI sourceURI, URI targetURI)
  {
    URI relativeSourceURI = targetURI.relativize(sourceURI);
    if (relativeSourceURI == sourceURI)
    {
      URI parentTargetURI = targetURI.resolve("..");
      if (parentTargetURI != targetURI)
      {
        URI parentRelativeSourceURI = relativize(relativeSourceURI, parentTargetURI);
        if (parentRelativeSourceURI != relativeSourceURI)
        {
          String string = parentRelativeSourceURI.toString();
          int index = string.indexOf('/');
          relativeSourceURI = URI.create(".." + string.substring(index));
        }
      }
    }
    return relativeSourceURI;
  }

  /**
   * Produces the {@code .xml.zx} forms as well as the {@code p2.index}.
   *
   * @param targetRepository
   */
  private void xzCompress(String targetRepository)
  {
    try
    {
      // We do this reflectively so that we can compile against Helios.
      //
      Bundle bundle = Platform.getBundle("org.eclipse.equinox.p2.repository.tools");
      Class<?> xzCompressorClass = bundle.loadClass("org.eclipse.equinox.p2.internal.repository.tools.XZCompressor");
      Object xzCompressor = xzCompressorClass.newInstance();
      xzCompressorClass.getMethod("setRepoFolder", String.class).invoke(xzCompressor, targetRepository);
      xzCompressorClass.getMethod("compressRepo").invoke(xzCompressor);
    }
    catch (Exception exception)
    {
      // Ignore.
    }
  }

  /**
   * Returns the qualified target folder depending on the build type and the give target subfolder.
   *
   * @param buildType the {@code publis.build.type}.
   * @param targetSubfolder the target subfolder.
   * @return the qualified target folder depending on the build type and the give target subfolder.
   */
  private String getTargetFolder(String buildType, String targetSubfolder)
  {
    return BUILD_TYPE_QUALIFIERS.get(BUILD_TYPES.indexOf(buildType)) + targetSubfolder;
  }

  public static URI createURI(String path)
  {
    return createURI(new File(path));
  }

  public static URI createURI(File file)
  {
    try
    {
      // Java has a bad habit of adding a trailing "/" if the file exists as a directory.
      // We never want that because it gives different results depending on existence.
      URI uri = file.getCanonicalFile().toURI();
      String literal = uri.toString();
      return literal.endsWith("/") ? new URI(literal.substring(0, literal.length() - 1)) : uri;
    }
    catch (Exception exception)
    {
      throw new IllegalArgumentException(exception);
    }
  }

  /**
   * Generates the index.html for the target repository.
   * @param target the target repository.
   *
   * @throws Exception
   */
  public void generateIndex(String target) throws Exception
  {
    String indexHTML = new UpdateSiteIndex().generate(new UpdateSiteIndexGenerator(target));
    FileOutputStream out = new FileOutputStream(new File(target, "index.html"));
    try
    {
      byte[] bytes = indexHTML.getBytes("UTF-8");
      out.write(bytes);
    }
    finally
    {
      out.close();
    }

    File targetFile = new File(target);
    if (targetFile.isDirectory())
    {
      for (File child : targetFile.listFiles())
      {
        if (child.isDirectory() && (new File(child, "compositeContent.jar").isFile() || new File(child, "content.jar").isFile()))
        {
          generateIndex(child.getPath());
        }
      }
    }
  }

  /**
   * Generates a zip and SHA hashes for all simple repositories nested in the target folder.
   *
   * @param target the target folder.
   * @throws Exception
   */
  public void generateDownloads(String target) throws Exception
  {
    File targetFile = new File(target);
    if (targetFile.isDirectory())
    {
      if (new File(targetFile, "content.jar").isFile())
      {
        File archiveFile = getArchiveFile(target);
        if (!archiveFile.exists())
        {
          createArchive(target);
        }

        if (!getDigestFile(archiveFile.getPath(), "SHA-256").isFile())
        {
          createDigest(archiveFile, "SHA-256");
        }

        if (!getDigestFile(archiveFile.getPath(), "SHA-512").isFile())
        {
          createDigest(archiveFile, "SHA-512");
        }
      }
      else
      {
        for (File child : targetFile.listFiles())
        {
          if (child.isDirectory())
          {
            generateDownloads(child.getPath());
          }
        }
      }
    }
  }

  /**
   * Sorts the list of repository folders to ensure that they are semantically order.
   *
   * @param repositories the repository folders.
   */
  public static void sort(List<String> repositories)
  {
    Map<Long, String> orderedRepositories = new TreeMap<Long, String>();
    for (String repository : repositories)
    {
      String name = new File(repository).getName();
      if ("release".equals(name))
      {
        orderedRepositories.put(0L, repository);
      }
      else if ("milestone".equals(name))
      {
        orderedRepositories.put(1L, repository);
      }
      else if ("nightly".equals(name))
      {
        orderedRepositories.put(2L, repository);
      }
      else if ("latest".equals(name))
      {
        orderedRepositories.put(Long.MIN_VALUE, repository);
      }
      else if (name.charAt(0) == 'N' || name.charAt(0) == 'S')
      {
        orderedRepositories.put(-Long.parseLong(name.substring(1)), repository);
      }
      else if (name.startsWith("2."))
      {
        orderedRepositories.put(-Long.parseLong(name.substring(2)), repository);
      }
      else
      {
        throw new IllegalArgumentException(repository);
      }
    }

    repositories.clear();
    repositories.addAll(orderedRepositories.values());
  }

  /**
   * Returns the {@link File#getCanonicalPath() canonical path} with normalized path separators, i.e., always '/'.
   * @param file the file.
   * @return the canonical path.
   *
   * @throws IOException
   */
  public static String getCanonicalPath(File file) throws IOException
  {
    return file.getCanonicalPath().replace('\\', '/');
  }

  /**
   * Returns the name of the zipped archive for the give simple repository.
   *
   * @param repository the simple repository.
   * @return
   */
  public static File getArchiveFile(String repository)
  {
    File file = new File(repository);
    String name = "EMF-Updates-" + file.getName() + ".zip";
    return new File(file, name);
  }

  /**
   * Creates a zip archive for the simple repository.
   *
   * @param repository the simple repository
   * @return the created archive.
   *
   * @throws IOException
   */
  public static File createArchive(String repository) throws IOException
  {
    File archiveFile = getArchiveFile(repository);
    File folder = archiveFile.getParentFile();
    if (!folder.isDirectory() || !new File(folder, "content.jar").isFile())
    {
      throw new IllegalStateException(repository + "is not a valid p2 repository");
    }

    boolean delete = false;
    FileOutputStream fileOutputStream = null;
    ZipOutputStream zipOutputStream = null;
    try
    {
      fileOutputStream = new FileOutputStream(archiveFile);
      zipOutputStream = new ZipOutputStream(fileOutputStream);
      for (File file : folder.listFiles())
      {
        String name = file.getName();
        if (UPDATE_SITE_CONTENT.contains(name))
        {
          visit(zipOutputStream, folder, name);
        }
      }
    }
    catch (IOException exception)
    {
      delete = true;
    }
    finally
    {
      if (zipOutputStream != null)
      {
        zipOutputStream.close();
      }
      if (fileOutputStream != null)
      {
        fileOutputStream.close();
      }
      if (delete)
      {
        archiveFile.delete();
      }
    }

    return archiveFile;
  }

  /**
   * Visits the folders recursively to zip all files.
   *
   * @param zipOutputStream the target archive.
   * @param root the root at which we started visiting.
   * @param path the relative path we are currently visiting.
   *
   * @throws IOException
   */
  private static void visit(ZipOutputStream zipOutputStream, File root, String path) throws IOException
  {
    File file = new File(root, path);
    if (file.isFile())
    {
      ZipEntry zipEntry = new ZipEntry(path);
      zipOutputStream.putNextEntry(zipEntry);
      FileInputStream fileInputStream = null;
      try
      {
        fileInputStream = new FileInputStream(file);
        byte[] bytes = new byte [10000];
        for (int length = fileInputStream.read(bytes); length != -1; length = fileInputStream.read(bytes))
        {
          zipOutputStream.write(bytes, 0, length);
        }
        zipOutputStream.closeEntry();
      }
      finally
      {
        if (fileInputStream != null)
        {
          fileInputStream.close();
        }
      }
    }
    else
    {
      for (String name : file.list())
      {
        visit(zipOutputStream, root, path + "/" + name);
      }
    }
  }

  /**
   * Returns the file location at which a digest for the given algorithm will be generated.
   * @param target the target file for the digest.
   * @param algorithm the algorithm used to compute the digest.
   * @return
   */
  public static File getDigestFile(String target, String algorithm)
  {
    File file = new File(target);
    File result = new File(file.getParentFile(), file.getName() + "." + algorithm.toLowerCase().replaceAll("-", ""));
    return result;
  }

  /**
   * Creates a file containing the digest for the given file using the given algorithm.
   *
   * @param file the file to digest.
   * @param algorithm the algorithm to use for digesting.
   * @return the location of the digest file.
   *
   * @throws IOException
   */
  private static File createDigest(File file, String algorithm) throws IOException
  {
    File result = getDigestFile(file.getPath(), algorithm);
    try
    {
      MessageDigest instance = MessageDigest.getInstance(algorithm);
      FileInputStream in = null;
      FileOutputStream out = null;
      try
      {
        in = new FileInputStream(file);
        byte[] bytes = new byte [10000];
        for (int length = in.read(bytes); length != -1; length = in.read(bytes))
        {
          instance.update(bytes, 0, length);
        }
        byte[] digest = instance.digest();

        StringBuilder body = new StringBuilder();
        for (int i = 0; i < digest.length; ++i)
        {
          body.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3));
        }

        body.append(" *");
        body.append(file.getName());
        out = new FileOutputStream(result);
        out.write(body.toString().getBytes("UTF-8"));
      }
      finally
      {
        if (in != null)
        {
          in.close();
        }

        if (out != null)
        {
          out.close();
        }
      }
    }
    catch (NoSuchAlgorithmException exception)
    {
      throw new IllegalArgumentException(exception);
    }

    return result;
  }

  /**
   * A utility class used to load a repository in order to analyze its contents.
   */
  public static class RepositoryAnalyzer extends AbstractApplication
  {
    /**
     * The pattern for finding the commit ID in a branding plugin's {@code about.mappings}.
     */
    private static final Pattern COMMIT_ID_PATTERN = Pattern.compile("^1=(.*)$", Pattern.MULTILINE);

    /**
     * The pattern for a well-formed Git commit ID.
     */
    private static final Pattern VALID_COMMIT_ID_PATTERN = Pattern.compile("^([0-9a-fA-F]+)$");

    /**
     * The pattern for finding the commit ID in a branding plugin's {@code about.mappings}.
     */
    private static final Pattern BUILD_ID_PATTERN = Pattern.compile("^0=(.*)$", Pattern.MULTILINE);

    /**
     * The pattern for a well-formed build ID.
     */
    private static final Pattern VALID_BUILD_ID_PATTERN = Pattern.compile("^[MNRS]?(\\d{12})$");

    /**
     * The pattern for finding a child location in a {@code compositeContent.xml}.
     */
    private static final Pattern CHILD_LOCATION_PATTERN = Pattern.compile("<child location='([^']*)'");

    /**
     * Returns the repository name.
     *
     * @return the repository name.
     */
    public String getName()
    {
      return getMetadataRepository().getName();
    }

    /**
     * Returns the two-segment version of the largest version of the {@code org.eclipse.emf.sdk.feature.group} in the repository.
     *
     * @return the version of the EMF SDK.
     *
     * @throws ProvisionException
     */
    public String getVersion() throws ProvisionException
    {
      IMetadataRepository repository = getCompositeMetadataRepository();
      IQueryResult<IInstallableUnit> query = repository.query(QueryUtil.createIUQuery("org.eclipse.emf.sdk.feature.group"), new NullProgressMonitor());
      BasicVersion maxVersion = null;
      for (Iterator<IInstallableUnit> i = query.iterator(); i.hasNext();)
      {
        IInstallableUnit iu = i.next();
        Version version = iu.getVersion();
        if (version instanceof BasicVersion)
        {
          BasicVersion basicVersion = (BasicVersion)version;
          if (maxVersion == null || maxVersion.compareTo(basicVersion) < 0)
          {
            maxVersion = basicVersion;
          }
        }
      }

      return maxVersion.getMajor() + "." + maxVersion.getMinor();
    }

    /**
     * Returns the list of child locations in the composite, or {@code null}, if the repository isn't a composite.
     * The URIs in the repository must be file locations and those are the locations returned.
     *
     * @return the list of child locations in the composite, or {@code null}, if the repository isn't a composite.
     */
    public List<String> getChildren()
    {
      IMetadataRepository metadataRepository = getMetadataRepository();
      URI location = metadataRepository.getLocation();
      File file = new File(new File(location), "compositeContent.jar");
      if (file.isFile() && metadataRepository instanceof ICompositeRepository<?>)
      {
        List<String> result = new ArrayList<String>();
        ICompositeRepository<?> compositeRepository = (ICompositeRepository<?>)metadataRepository;
        List<URI> children = compositeRepository.getChildren();
        for (URI uri : children)
        {
          try
          {
            result.add(getCanonicalPath(new File(uri)));
          }
          catch (IOException exception)
          {
            throw new IllegalStateException("The child '" + uri + "' of '" + location + "' is not a file in the file system.", exception);
          }
        }
        return result;
      }
      else
      {
        return null;
      }
    }

    /**
     * Returns a list of the actual value of each child element's location attribute in the composite, or {@code null} if the repository is not a composite.
     *
     * @return a list of the actual value of each child element's location attribute in the composite, or {@code null} if the repository is not a composite.
     */
    public List<String> getRawChildren()
    {
      IMetadataRepository metadataRepository = getMetadataRepository();
      URI location = metadataRepository.getLocation();
      File file = new File(new File(location), "compositeContent.jar");
      if (file.isFile())
      {
        List<String> result = new ArrayList<String>();
        ZipFile zipFile = null;
        try
        {
          zipFile = new ZipFile(file);
          ZipEntry entry = zipFile.getEntry("compositeContent.xml");
          InputStream inputStream = zipFile.getInputStream(entry);
          ByteArrayOutputStream out = new ByteArrayOutputStream();
          byte[] bytes = new byte [10000];
          for (int length = inputStream.read(bytes); length != -1; length = inputStream.read(bytes))
          {
            out.write(bytes, 0, length);
          }

          String content = new String(bytes, "UTF-8");
          for (Matcher matcher = CHILD_LOCATION_PATTERN.matcher(content); matcher.find();)
          {
            result.add(matcher.group(1));
          }
        }
        catch (Exception exception)
        {
          throw new IllegalStateException("Problems with " + file, exception);
        }
        finally
        {
          try
          {
            zipFile.close();
          }
          catch (IOException exception)
          {
            throw new IllegalStateException("Problems closing" + file, exception);
          }
        }

        return result;
      }
      else
      {
        return null;
      }
    }

    /**
     * Returns the sorted list of all the SDK features in the repository.
     * @return the sorted list of all the SDK features in the repository.
     */
    public List<String> getSDKs()
    {
      List<String> result = new ArrayList<String>();
      IMetadataRepository repository = getCompositeMetadataRepository();
      IQueryResult<IInstallableUnit> query = repository.query(QueryUtil.createIUGroupQuery(), new NullProgressMonitor());
      for (Iterator<IInstallableUnit> i = query.iterator(); i.hasNext();)
      {
        IInstallableUnit iu = i.next();
        if (iu.getId().endsWith(".sdk.feature.group"))
        {
          String name = iu.getProperty("org.eclipse.equinox.p2.name", null);
          if (!result.contains(name))
          {
            result.add(name);
          }
        }
      }
      Collections.sort(result);
      return result;
    }

    /**
     * Returns a sorted list of all the features in the repository.
     * @return a sorted list of all the features in the repository.
     */
    public List<String> getFeatures()
    {
      List<String> result = new ArrayList<String>();
      IMetadataRepository repository = getCompositeMetadataRepository();
      IQueryResult<IInstallableUnit> query = repository.query(QueryUtil.createIUGroupQuery(), new NullProgressMonitor());
      for (Iterator<IInstallableUnit> i = query.iterator(); i.hasNext();)
      {
        IInstallableUnit iu = i.next();
        if (!iu.getId().endsWith(".source.feature.group"))
        {
          String name = iu.getProperty("org.eclipse.equinox.p2.name", null);
          name += " " + iu.getVersion();
          name = name.substring(0, name.lastIndexOf('.'));
          if (!result.contains(name))
          {
            result.add(name);
          }
        }
      }
      Collections.sort(result);
      return result;
    }

    /**
     * Returns a map from bundle name to a list of information for that bundle for each bundle in the repository.
     * @return a map from bundle name to a list of information for that bundle for each bundle in the repository.
     */
    public Map<String, List<String>> getBundles()
    {
      Map<String, List<String>> result = new TreeMap<String, List<String>>();
      IMetadataRepository repository = getCompositeMetadataRepository();
      IQueryResult<IInstallableUnit> query = repository.query(QueryUtil.createIUAnyQuery(), new NullProgressMonitor());
      for (Iterator<IInstallableUnit> i = query.iterator(); i.hasNext();)
      {
        IInstallableUnit iu = i.next();
        String id = iu.getId();
        if (!id.endsWith(".source"))
        {
          List<String> lines = new ArrayList<String>();
          for (IProvidedCapability providedCapability : iu.getProvidedCapabilities())
          {
            String namespace = providedCapability.getNamespace();
            String name = providedCapability.getName();
            if ("org.eclipse.equinox.p2.eclipse.type".equals(namespace) && "bundle".equals(name))
            {
              String iuName = iu.getProperty("org.eclipse.equinox.p2.name", null);
              iuName += " " + iu.getVersion();
              iuName = iuName.substring(0, iuName.lastIndexOf('.'));
              if (!result.containsKey(iuName))
              {
                lines.add(0, "\u21D6 " + id + " " + iu.getVersion());
                result.put(iuName, lines);
              }
            }
            else if ("java.package".equals(namespace))
            {
              Version version = providedCapability.getVersion();
              lines.add("\u2196 " + name + (Version.emptyVersion.equals(version) ? "" : " " + version));
            }
          }

          for (IRequirement requirement : iu.getRequirements())
          {
            if (requirement instanceof IRequiredCapability)
            {
              IRequiredCapability requiredCapability = (IRequiredCapability)requirement;
              String namespace = requiredCapability.getNamespace();
              String line = null;
              if ("osgi.bundle".equals(namespace))
              {
                line = "\u21D8 ";
              }
              else if ("java.package".equals(namespace))
              {
                line = "\u2198 ";
              }
              if (line != null)
              {
                String name = requiredCapability.getName();
                VersionRange range = requiredCapability.getRange();

                line += name;
                if (!VersionRange.emptyRange.equals(range))
                {
                  line += " " + range;
                }

                if (requiredCapability.getMin() == 0)
                {
                  line += " optional";
                  if (requiredCapability.isGreedy())
                  {
                    line += " greedy";
                  }
                }

                lines.add(line);
              }
            }
          }
        }
      }
      return result;
    }

    /**
     * Returns a map from project name to the URL for the commit ID URL in that project's branding plugin.
     * @return a map from project name to the URL for the commit ID URL in that project's branding plugin.
     */
    public Map<String, String> getCommits()
    {
      Map<String, String> result = new LinkedHashMap<String, String>();
      getIDs(result, "org.eclipse.emf", COMMIT_ID_PATTERN, VALID_COMMIT_ID_PATTERN, "http://git.eclipse.org/c/emf/org.eclipse.emf.git/commit/?id=");
      getIDs(result, "org.eclipse.xsd", COMMIT_ID_PATTERN, VALID_COMMIT_ID_PATTERN, "http://git.eclipse.org/c/xsd/org.eclipse.xsd.git/commit/?id=");
      getDate();
      return result;
    }

    /**
     * Returns the build date as determined from the EMF project's branding plugin.
     * @return the build date as determined from the EMF project's branding plugin.
     */
    public String getDate()
    {
      Map<String, String> result = new LinkedHashMap<String, String>();
      getIDs(result, "org.eclipse.emf", BUILD_ID_PATTERN, VALID_BUILD_ID_PATTERN, "");
      if (!result.isEmpty())
      {
        String value = result.values().iterator().next();
        try
        {
          Date date = new SimpleDateFormat("yyyyMMddHHmm").parse(value);
          return new SimpleDateFormat("yyyy'-'MM'-'dd' at 'HH':'mm ").format(date);
        }
        catch (ParseException e)
        {
          // Ignore.
        }
      }

      return null;
    }

    /**
     * Populates the IDs with the information from {@code about.mappings} for the branding plugin with the give UI ID.
     * @param ids the IDs to populate.
     * @param iuID the ID of a branding plugin.
     * @param idPattern the pattern for finding the ID in the {@code about.mappings}
     * @param validIDPattern the pattern for validating and extracting the ID.
     * @param prefix the prefix that will be prepended to the ID.
     */
    private void getIDs(Map<String, String> ids, String iuID, Pattern idPattern, Pattern validIDPattern, String prefix)
    {
      IMetadataRepository metadataRepository = getCompositeMetadataRepository();
      IArtifactRepository artifactRepository = getCompositeArtifactRepository();
      IQueryResult<IInstallableUnit> ius = metadataRepository.query(QueryUtil.createIUQuery(iuID), new NullProgressMonitor());
      for (Iterator<IInstallableUnit> i = ius.iterator(); i.hasNext();)
      {
        IInstallableUnit iu = i.next();
        Collection<IArtifactKey> artifacts = iu.getArtifacts();
        for (IArtifactKey artifactKey : artifacts)
        {
          IArtifactDescriptor[] artifactDescriptors = artifactRepository.getArtifactDescriptors(artifactKey);
          for (IArtifactDescriptor artifactDescriptor : artifactDescriptors)
          {
            ByteArrayOutputStream artifact = new ByteArrayOutputStream();
            artifactRepository.getArtifact(artifactDescriptor, artifact, new NullProgressMonitor());
            ZipInputStream zipInputStream = null;
            try
            {
              zipInputStream = new ZipInputStream(new ByteArrayInputStream(artifact.toByteArray()));
              for (ZipEntry entry = zipInputStream.getNextEntry(); entry != null; entry = zipInputStream.getNextEntry())
              {
                String name = entry.getName();
                if ("about.mappings".equals(name))
                {
                  ByteArrayOutputStream content = new ByteArrayOutputStream();
                  byte[] bytes = new byte [1024];
                  for (int length = zipInputStream.read(bytes); length != -1; length = zipInputStream.read(bytes))
                  {
                    content.write(bytes, 0, length);
                  }

                  content.close();
                  String value = new String(content.toByteArray(), "UTF-8");
                  for (Matcher matcher = idPattern.matcher(value); matcher.find();)
                  {
                    String id = matcher.group(1);
                    Matcher idMatcher = validIDPattern.matcher(id);
                    if (idMatcher.matches())
                    {
                      ids.put(iuID.substring(iuID.lastIndexOf('.') + 1).toUpperCase(), prefix + idMatcher.group(1));
                    }
                    break;
                  }
                  break;
                }
              }
            }
            catch (IOException exception)
            {
              // Ignore.
            }
            finally
            {
              if (zipInputStream != null)
              {
                try
                {
                  zipInputStream.close();
                }
                catch (IOException e)
                {
                  // Ignore.
                }
              }
            }
            break;
          }
        }

        break;
      }
    }

    /**
     * Returns the metadata repository of this analyzer.
     * @return the metadata repository of this analyzer.
     */
    private IMetadataRepository getMetadataRepository()
    {
      ICompositeRepository<?> compositeMetadataRepository = (ICompositeRepository<?>)getCompositeMetadataRepository();
      IMetadataRepositoryManager metadataRepositoryManager = getMetadataRepositoryManager();
      try
      {
        return metadataRepositoryManager.loadRepository(compositeMetadataRepository.getChildren().get(0), new NullProgressMonitor());
      }
      catch (Exception exception)
      {
        throw new IllegalStateException(exception);
      }
    }

    @Override
    public IStatus run(IProgressMonitor monitor) throws ProvisionException
    {
      return null;
    }
  }
}
