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 javax.xml.parsers.ParserConfigurationException;
  26. import javax.xml.parsers.SAXParserFactory;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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