ManifestParser.java

  1. /*
  2.  * Copyright (C) 2015, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.gitrepo;

  11. import java.io.FileInputStream;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.net.URI;
  15. import java.net.URISyntaxException;
  16. import java.text.MessageFormat;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;

  25. import org.eclipse.jgit.annotations.NonNull;
  26. import org.eclipse.jgit.api.errors.GitAPIException;
  27. import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
  28. import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
  29. import org.eclipse.jgit.gitrepo.RepoProject.ReferenceFile;
  30. import org.eclipse.jgit.gitrepo.internal.RepoText;
  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.lib.Repository;
  33. import org.xml.sax.Attributes;
  34. import org.xml.sax.InputSource;
  35. import org.xml.sax.SAXException;
  36. import org.xml.sax.XMLReader;
  37. import org.xml.sax.helpers.DefaultHandler;
  38. import org.xml.sax.helpers.XMLReaderFactory;

  39. /**
  40.  * Repo XML manifest parser.
  41.  *
  42.  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
  43.  * @since 4.0
  44.  */
  45. public class ManifestParser extends DefaultHandler {
  46.     private final String filename;
  47.     private final URI baseUrl;
  48.     private final String defaultBranch;
  49.     private final Repository rootRepo;
  50.     private final Map<String, Remote> remotes;
  51.     private final Set<String> plusGroups;
  52.     private final Set<String> minusGroups;
  53.     private final List<RepoProject> projects;
  54.     private final List<RepoProject> filteredProjects;
  55.     private final IncludedFileReader includedReader;

  56.     private String defaultRemote;
  57.     private String defaultRevision;
  58.     private int xmlInRead;
  59.     private RepoProject currentProject;

  60.     /**
  61.      * A callback to read included xml files.
  62.      */
  63.     public interface IncludedFileReader {
  64.         /**
  65.          * Read a file from the same base dir of the manifest xml file.
  66.          *
  67.          * @param path
  68.          *            The relative path to the file to read
  69.          * @return the {@code InputStream} of the file.
  70.          * @throws GitAPIException
  71.          * @throws IOException
  72.          */
  73.         public InputStream readIncludeFile(String path)
  74.                 throws GitAPIException, IOException;
  75.     }

  76.     /**
  77.      * Constructor for ManifestParser
  78.      *
  79.      * @param includedReader
  80.      *            a
  81.      *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
  82.      *            object.
  83.      * @param filename
  84.      *            a {@link java.lang.String} object.
  85.      * @param defaultBranch
  86.      *            a {@link java.lang.String} object.
  87.      * @param baseUrl
  88.      *            a {@link java.lang.String} object.
  89.      * @param groups
  90.      *            a {@link java.lang.String} object.
  91.      * @param rootRepo
  92.      *            a {@link org.eclipse.jgit.lib.Repository} object.
  93.      */
  94.     public ManifestParser(IncludedFileReader includedReader, String filename,
  95.             String defaultBranch, String baseUrl, String groups,
  96.             Repository rootRepo) {
  97.         this.includedReader = includedReader;
  98.         this.filename = filename;
  99.         this.defaultBranch = defaultBranch;
  100.         this.rootRepo = rootRepo;
  101.         this.baseUrl = normalizeEmptyPath(URI.create(baseUrl));

  102.         plusGroups = new HashSet<>();
  103.         minusGroups = new HashSet<>();
  104.         if (groups == null || groups.length() == 0
  105.                 || groups.equals("default")) { //$NON-NLS-1$
  106.             // default means "all,-notdefault"
  107.             minusGroups.add("notdefault"); //$NON-NLS-1$
  108.         } else {
  109.             for (String group : groups.split(",")) { //$NON-NLS-1$
  110.                 if (group.startsWith("-")) //$NON-NLS-1$
  111.                     minusGroups.add(group.substring(1));
  112.                 else
  113.                     plusGroups.add(group);
  114.             }
  115.         }

  116.         remotes = new HashMap<>();
  117.         projects = new ArrayList<>();
  118.         filteredProjects = new ArrayList<>();
  119.     }

  120.     /**
  121.      * Read the xml file.
  122.      *
  123.      * @param inputStream
  124.      *            a {@link java.io.InputStream} object.
  125.      * @throws java.io.IOException
  126.      */
  127.     public void read(InputStream inputStream) throws IOException {
  128.         xmlInRead++;
  129.         final XMLReader xr;
  130.         try {
  131.             xr = XMLReaderFactory.createXMLReader();
  132.         } catch (SAXException e) {
  133.             throw new IOException(JGitText.get().noXMLParserAvailable, e);
  134.         }
  135.         xr.setContentHandler(this);
  136.         try {
  137.             xr.parse(new InputSource(inputStream));
  138.         } catch (SAXException e) {
  139.             throw new IOException(RepoText.get().errorParsingManifestFile, e);
  140.         }
  141.     }

  142.     /** {@inheritDoc} */
  143.     @SuppressWarnings("nls")
  144.     @Override
  145.     public void startElement(
  146.             String uri,
  147.             String localName,
  148.             String qName,
  149.             Attributes attributes) throws SAXException {
  150.         if (qName == null) {
  151.             return;
  152.         }
  153.         switch (qName) {
  154.         case "project":
  155.             if (attributes.getValue("name") == null) {
  156.                 throw new SAXException(RepoText.get().invalidManifest);
  157.             }
  158.             currentProject = new RepoProject(attributes.getValue("name"),
  159.                     attributes.getValue("path"),
  160.                     attributes.getValue("revision"),
  161.                     attributes.getValue("remote"),
  162.                     attributes.getValue("groups"));
  163.             currentProject
  164.                     .setRecommendShallow(attributes.getValue("clone-depth"));
  165.             break;
  166.         case "remote":
  167.             String alias = attributes.getValue("alias");
  168.             String fetch = attributes.getValue("fetch");
  169.             String revision = attributes.getValue("revision");
  170.             Remote remote = new Remote(fetch, revision);
  171.             remotes.put(attributes.getValue("name"), remote);
  172.             if (alias != null) {
  173.                 remotes.put(alias, remote);
  174.             }
  175.             break;
  176.         case "default":
  177.             defaultRemote = attributes.getValue("remote");
  178.             defaultRevision = attributes.getValue("revision");
  179.             break;
  180.         case "copyfile":
  181.             if (currentProject == null) {
  182.                 throw new SAXException(RepoText.get().invalidManifest);
  183.             }
  184.             currentProject.addCopyFile(new CopyFile(rootRepo,
  185.                     currentProject.getPath(), attributes.getValue("src"),
  186.                     attributes.getValue("dest")));
  187.             break;
  188.         case "linkfile":
  189.             if (currentProject == null) {
  190.                 throw new SAXException(RepoText.get().invalidManifest);
  191.             }
  192.             currentProject.addLinkFile(new LinkFile(rootRepo,
  193.                     currentProject.getPath(), attributes.getValue("src"),
  194.                     attributes.getValue("dest")));
  195.             break;
  196.         case "include":
  197.             String name = attributes.getValue("name");
  198.             if (includedReader != null) {
  199.                 try (InputStream is = includedReader.readIncludeFile(name)) {
  200.                     if (is == null) {
  201.                         throw new SAXException(
  202.                                 RepoText.get().errorIncludeNotImplemented);
  203.                     }
  204.                     read(is);
  205.                 } catch (Exception e) {
  206.                     throw new SAXException(MessageFormat
  207.                             .format(RepoText.get().errorIncludeFile, name), e);
  208.                 }
  209.             } else if (filename != null) {
  210.                 int index = filename.lastIndexOf('/');
  211.                 String path = filename.substring(0, index + 1) + name;
  212.                 try (InputStream is = new FileInputStream(path)) {
  213.                     read(is);
  214.                 } catch (IOException e) {
  215.                     throw new SAXException(MessageFormat
  216.                             .format(RepoText.get().errorIncludeFile, path), e);
  217.                 }
  218.             }
  219.             break;
  220.         case "remove-project": {
  221.             String name2 = attributes.getValue("name");
  222.             projects.removeIf((p) -> p.getName().equals(name2));
  223.             break;
  224.         }
  225.         default:
  226.             break;
  227.         }
  228.     }

  229.     /** {@inheritDoc} */
  230.     @Override
  231.     public void endElement(
  232.             String uri,
  233.             String localName,
  234.             String qName) throws SAXException {
  235.         if ("project".equals(qName)) { //$NON-NLS-1$
  236.             projects.add(currentProject);
  237.             currentProject = null;
  238.         }
  239.     }

  240.     /** {@inheritDoc} */
  241.     @Override
  242.     public void endDocument() throws SAXException {
  243.         xmlInRead--;
  244.         if (xmlInRead != 0)
  245.             return;

  246.         // Only do the following after we finished reading everything.
  247.         Map<String, URI> remoteUrls = new HashMap<>();
  248.         if (defaultRevision == null && defaultRemote != null) {
  249.             Remote remote = remotes.get(defaultRemote);
  250.             if (remote != null) {
  251.                 defaultRevision = remote.revision;
  252.             }
  253.             if (defaultRevision == null) {
  254.                 defaultRevision = defaultBranch;
  255.             }
  256.         }
  257.         for (RepoProject proj : projects) {
  258.             String remote = proj.getRemote();
  259.             String revision = defaultRevision;
  260.             if (remote == null) {
  261.                 if (defaultRemote == null) {
  262.                     if (filename != null) {
  263.                         throw new SAXException(MessageFormat.format(
  264.                                 RepoText.get().errorNoDefaultFilename,
  265.                                 filename));
  266.                     }
  267.                     throw new SAXException(RepoText.get().errorNoDefault);
  268.                 }
  269.                 remote = defaultRemote;
  270.             } else {
  271.                 Remote r = remotes.get(remote);
  272.                 if (r != null && r.revision != null) {
  273.                     revision = r.revision;
  274.                 }
  275.             }
  276.             URI remoteUrl = remoteUrls.get(remote);
  277.             if (remoteUrl == null) {
  278.                 String fetch = remotes.get(remote).fetch;
  279.                 if (fetch == null) {
  280.                     throw new SAXException(MessageFormat
  281.                             .format(RepoText.get().errorNoFetch, remote));
  282.                 }
  283.                 remoteUrl = normalizeEmptyPath(baseUrl.resolve(fetch));
  284.                 remoteUrls.put(remote, remoteUrl);
  285.             }
  286.             proj.setUrl(remoteUrl.resolve(proj.getName()).toString())
  287.                 .setDefaultRevision(revision);
  288.         }

  289.         filteredProjects.addAll(projects);
  290.         removeNotInGroup();
  291.         removeOverlaps();
  292.     }

  293.     static URI normalizeEmptyPath(URI u) {
  294.         // URI.create("scheme://host").resolve("a/b") => "scheme://hosta/b"
  295.         // That seems like bug https://bugs.openjdk.java.net/browse/JDK-4666701.
  296.         // We workaround this by special casing the empty path case.
  297.         if (u.getHost() != null && !u.getHost().isEmpty() &&
  298.             (u.getPath() == null || u.getPath().isEmpty())) {
  299.             try {
  300.                 return new URI(u.getScheme(),
  301.                     u.getUserInfo(), u.getHost(), u.getPort(),
  302.                         "/", u.getQuery(), u.getFragment()); //$NON-NLS-1$
  303.             } catch (URISyntaxException x) {
  304.                 throw new IllegalArgumentException(x.getMessage(), x);
  305.             }
  306.         }
  307.         return u;
  308.     }

  309.     /**
  310.      * Getter for projects.
  311.      *
  312.      * @return projects list reference, never null
  313.      */
  314.     public List<RepoProject> getProjects() {
  315.         return projects;
  316.     }

  317.     /**
  318.      * Getter for filterdProjects.
  319.      *
  320.      * @return filtered projects list reference, never null
  321.      */
  322.     @NonNull
  323.     public List<RepoProject> getFilteredProjects() {
  324.         return filteredProjects;
  325.     }

  326.     /** Remove projects that are not in our desired groups. */
  327.     void removeNotInGroup() {
  328.         Iterator<RepoProject> iter = filteredProjects.iterator();
  329.         while (iter.hasNext())
  330.             if (!inGroups(iter.next()))
  331.                 iter.remove();
  332.     }

  333.     /** Remove projects that sits in a subdirectory of any other project. */
  334.     void removeOverlaps() {
  335.         Collections.sort(filteredProjects);
  336.         Iterator<RepoProject> iter = filteredProjects.iterator();
  337.         if (!iter.hasNext())
  338.             return;
  339.         RepoProject last = iter.next();
  340.         while (iter.hasNext()) {
  341.             RepoProject p = iter.next();
  342.             if (last.isAncestorOf(p))
  343.                 iter.remove();
  344.             else
  345.                 last = p;
  346.         }
  347.         removeNestedCopyAndLinkfiles();
  348.     }

  349.     private void removeNestedCopyAndLinkfiles() {
  350.         for (RepoProject proj : filteredProjects) {
  351.             List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
  352.             proj.clearCopyFiles();
  353.             for (CopyFile copyfile : copyfiles) {
  354.                 if (!isNestedReferencefile(copyfile)) {
  355.                     proj.addCopyFile(copyfile);
  356.                 }
  357.             }
  358.             List<LinkFile> linkfiles = new ArrayList<>(proj.getLinkFiles());
  359.             proj.clearLinkFiles();
  360.             for (LinkFile linkfile : linkfiles) {
  361.                 if (!isNestedReferencefile(linkfile)) {
  362.                     proj.addLinkFile(linkfile);
  363.                 }
  364.             }
  365.         }
  366.     }

  367.     boolean inGroups(RepoProject proj) {
  368.         for (String group : minusGroups) {
  369.             if (proj.inGroup(group)) {
  370.                 // minus groups have highest priority.
  371.                 return false;
  372.             }
  373.         }
  374.         if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
  375.             // empty plus groups means "all"
  376.             return true;
  377.         }
  378.         for (String group : plusGroups) {
  379.             if (proj.inGroup(group))
  380.                 return true;
  381.         }
  382.         return false;
  383.     }

  384.     private boolean isNestedReferencefile(ReferenceFile referencefile) {
  385.         if (referencefile.dest.indexOf('/') == -1) {
  386.             // If the referencefile is at root level then it won't be nested.
  387.             return false;
  388.         }
  389.         for (RepoProject proj : filteredProjects) {
  390.             if (proj.getPath().compareTo(referencefile.dest) > 0) {
  391.                 // Early return as remaining projects can't be ancestor of this
  392.                 // referencefile config (filteredProjects is sorted).
  393.                 return false;
  394.             }
  395.             if (proj.isAncestorOf(referencefile.dest)) {
  396.                 return true;
  397.             }
  398.         }
  399.         return false;
  400.     }

  401.     private static class Remote {
  402.         final String fetch;
  403.         final String revision;

  404.         Remote(String fetch, String revision) {
  405.             this.fetch = fetch;
  406.             this.revision = revision;
  407.         }
  408.     }
  409. }