aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java449
1 files changed, 449 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
new file mode 100644
index 0000000000..58b4d3dc56
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2015, Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gitrepo;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
+import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
+import org.eclipse.jgit.gitrepo.RepoProject.ReferenceFile;
+import org.eclipse.jgit.gitrepo.internal.RepoText;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Repo XML manifest parser.
+ *
+ * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
+ * @since 4.0
+ */
+public class ManifestParser extends DefaultHandler {
+ private final String filename;
+ private final URI baseUrl;
+ private final String defaultBranch;
+ private final Repository rootRepo;
+ private final Map<String, Remote> remotes;
+ private final Set<String> plusGroups;
+ private final Set<String> minusGroups;
+ private final List<RepoProject> projects;
+ private final List<RepoProject> filteredProjects;
+ private final IncludedFileReader includedReader;
+
+ private String defaultRemote;
+ private String defaultRevision;
+ private int xmlInRead;
+ private RepoProject currentProject;
+
+ /**
+ * A callback to read included xml files.
+ */
+ public interface IncludedFileReader {
+ /**
+ * Read a file from the same base dir of the manifest xml file.
+ *
+ * @param path
+ * The relative path to the file to read
+ * @return the {@code InputStream} of the file.
+ * @throws GitAPIException
+ * a JGit API exception
+ * @throws IOException
+ * if an IO error occurred
+ */
+ public InputStream readIncludeFile(String path)
+ throws GitAPIException, IOException;
+ }
+
+ /**
+ * Constructor for ManifestParser
+ *
+ * @param includedReader
+ * a
+ * {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
+ * object.
+ * @param filename
+ * a {@link java.lang.String} object.
+ * @param defaultBranch
+ * a {@link java.lang.String} object.
+ * @param baseUrl
+ * a {@link java.lang.String} object.
+ * @param groups
+ * a {@link java.lang.String} object.
+ * @param rootRepo
+ * a {@link org.eclipse.jgit.lib.Repository} object.
+ */
+ public ManifestParser(IncludedFileReader includedReader, String filename,
+ String defaultBranch, String baseUrl, String groups,
+ Repository rootRepo) {
+ this.includedReader = includedReader;
+ this.filename = filename;
+ this.defaultBranch = defaultBranch;
+ this.rootRepo = rootRepo;
+ this.baseUrl = normalizeEmptyPath(URI.create(baseUrl));
+
+ plusGroups = new HashSet<>();
+ minusGroups = new HashSet<>();
+ if (groups == null || groups.length() == 0
+ || groups.equals("default")) { //$NON-NLS-1$
+ // default means "all,-notdefault"
+ minusGroups.add("notdefault"); //$NON-NLS-1$
+ } else {
+ for (String group : groups.split(",")) { //$NON-NLS-1$
+ if (group.startsWith("-")) //$NON-NLS-1$
+ minusGroups.add(group.substring(1));
+ else
+ plusGroups.add(group);
+ }
+ }
+
+ remotes = new HashMap<>();
+ projects = new ArrayList<>();
+ filteredProjects = new ArrayList<>();
+ }
+
+ /**
+ * Read the xml file.
+ *
+ * @param inputStream
+ * a {@link java.io.InputStream} object.
+ * @throws java.io.IOException
+ * if an IO error occurred
+ */
+ public void read(InputStream inputStream) throws IOException {
+ xmlInRead++;
+ final XMLReader xr;
+ try {
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ spf.setFeature(
+ "http://xml.org/sax/features/external-general-entities", //$NON-NLS-1$
+ false);
+ spf.setFeature(
+ "http://xml.org/sax/features/external-parameter-entities", //$NON-NLS-1$
+ false);
+ spf.setFeature(
+ "http://apache.org/xml/features/disallow-doctype-decl", //$NON-NLS-1$
+ true);
+ xr = spf.newSAXParser().getXMLReader();
+ } catch (SAXException | ParserConfigurationException e) {
+ throw new IOException(JGitText.get().noXMLParserAvailable, e);
+ }
+ xr.setContentHandler(this);
+ try {
+ xr.parse(new InputSource(inputStream));
+ } catch (SAXException e) {
+ throw new IOException(RepoText.get().errorParsingManifestFile, e);
+ }
+ }
+
+ @SuppressWarnings("nls")
+ @Override
+ public void startElement(
+ String uri,
+ String localName,
+ String qName,
+ Attributes attributes) throws SAXException {
+ if (qName == null) {
+ return;
+ }
+ switch (qName) {
+ case "project":
+ if (attributes.getValue("name") == null) {
+ throw new SAXException(RepoText.get().invalidManifest);
+ }
+ currentProject = new RepoProject(attributes.getValue("name"),
+ attributes.getValue("path"),
+ attributes.getValue("revision"),
+ attributes.getValue("remote"),
+ attributes.getValue("groups"));
+ currentProject
+ .setRecommendShallow(attributes.getValue("clone-depth"));
+ currentProject
+ .setUpstream(attributes.getValue("upstream"));
+ currentProject
+ .setDestBranch(attributes.getValue("dest-branch"));
+ break;
+ case "remote":
+ String alias = attributes.getValue("alias");
+ String fetch = attributes.getValue("fetch");
+ String revision = attributes.getValue("revision");
+ Remote remote = new Remote(fetch, revision);
+ remotes.put(attributes.getValue("name"), remote);
+ if (alias != null) {
+ remotes.put(alias, remote);
+ }
+ break;
+ case "default":
+ defaultRemote = attributes.getValue("remote");
+ defaultRevision = attributes.getValue("revision");
+ break;
+ case "copyfile":
+ if (currentProject == null) {
+ throw new SAXException(RepoText.get().invalidManifest);
+ }
+ currentProject.addCopyFile(new CopyFile(rootRepo,
+ currentProject.getPath(), attributes.getValue("src"),
+ attributes.getValue("dest")));
+ break;
+ case "linkfile":
+ if (currentProject == null) {
+ throw new SAXException(RepoText.get().invalidManifest);
+ }
+ currentProject.addLinkFile(new LinkFile(rootRepo,
+ currentProject.getPath(), attributes.getValue("src"),
+ attributes.getValue("dest")));
+ break;
+ case "include":
+ String name = attributes.getValue("name");
+ if (includedReader != null) {
+ try (InputStream is = includedReader.readIncludeFile(name)) {
+ if (is == null) {
+ throw new SAXException(
+ RepoText.get().errorIncludeNotImplemented);
+ }
+ read(is);
+ } catch (Exception e) {
+ throw new SAXException(MessageFormat
+ .format(RepoText.get().errorIncludeFile, name), e);
+ }
+ } else if (filename != null) {
+ int index = filename.lastIndexOf('/');
+ String path = filename.substring(0, index + 1) + name;
+ try (InputStream is = new FileInputStream(path)) {
+ read(is);
+ } catch (IOException e) {
+ throw new SAXException(MessageFormat
+ .format(RepoText.get().errorIncludeFile, path), e);
+ }
+ }
+ break;
+ case "remove-project": {
+ String name2 = attributes.getValue("name");
+ projects.removeIf((p) -> p.getName().equals(name2));
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void endElement(
+ String uri,
+ String localName,
+ String qName) throws SAXException {
+ if ("project".equals(qName)) { //$NON-NLS-1$
+ projects.add(currentProject);
+ currentProject = null;
+ }
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ xmlInRead--;
+ if (xmlInRead != 0)
+ return;
+
+ // Only do the following after we finished reading everything.
+ Map<String, URI> remoteUrls = new HashMap<>();
+ if (defaultRevision == null && defaultRemote != null) {
+ Remote remote = remotes.get(defaultRemote);
+ if (remote != null) {
+ defaultRevision = remote.revision;
+ }
+ if (defaultRevision == null) {
+ defaultRevision = defaultBranch;
+ }
+ }
+ for (RepoProject proj : projects) {
+ String remote = proj.getRemote();
+ String revision = defaultRevision;
+ if (remote == null) {
+ if (defaultRemote == null) {
+ if (filename != null) {
+ throw new SAXException(MessageFormat.format(
+ RepoText.get().errorNoDefaultFilename,
+ filename));
+ }
+ throw new SAXException(RepoText.get().errorNoDefault);
+ }
+ remote = defaultRemote;
+ } else {
+ Remote r = remotes.get(remote);
+ if (r != null && r.revision != null) {
+ revision = r.revision;
+ }
+ }
+ URI remoteUrl = remoteUrls.get(remote);
+ if (remoteUrl == null) {
+ String fetch = remotes.get(remote).fetch;
+ if (fetch == null) {
+ throw new SAXException(MessageFormat
+ .format(RepoText.get().errorNoFetch, remote));
+ }
+ remoteUrl = normalizeEmptyPath(baseUrl.resolve(fetch));
+ remoteUrls.put(remote, remoteUrl);
+ }
+ proj.setUrl(remoteUrl.resolve(proj.getName()).toString())
+ .setDefaultRevision(revision);
+ }
+
+ filteredProjects.addAll(projects);
+ removeNotInGroup();
+ removeOverlaps();
+ }
+
+ static URI normalizeEmptyPath(URI u) {
+ // URI.create("scheme://host").resolve("a/b") => "scheme://hosta/b"
+ // That seems like bug https://bugs.openjdk.java.net/browse/JDK-4666701.
+ // We workaround this by special casing the empty path case.
+ if (u.getHost() != null && !u.getHost().isEmpty() &&
+ (u.getPath() == null || u.getPath().isEmpty())) {
+ try {
+ return new URI(u.getScheme(),
+ u.getUserInfo(), u.getHost(), u.getPort(),
+ "/", u.getQuery(), u.getFragment()); //$NON-NLS-1$
+ } catch (URISyntaxException x) {
+ throw new IllegalArgumentException(x.getMessage(), x);
+ }
+ }
+ return u;
+ }
+
+ /**
+ * Getter for projects.
+ *
+ * @return projects list reference, never null
+ */
+ public List<RepoProject> getProjects() {
+ return projects;
+ }
+
+ /**
+ * Getter for filterdProjects.
+ *
+ * @return filtered projects list reference, never null
+ */
+ @NonNull
+ public List<RepoProject> getFilteredProjects() {
+ return filteredProjects;
+ }
+
+ /** Remove projects that are not in our desired groups. */
+ void removeNotInGroup() {
+ Iterator<RepoProject> iter = filteredProjects.iterator();
+ while (iter.hasNext())
+ if (!inGroups(iter.next()))
+ iter.remove();
+ }
+
+ /** Remove projects that sits in a subdirectory of any other project. */
+ void removeOverlaps() {
+ Collections.sort(filteredProjects);
+ Iterator<RepoProject> iter = filteredProjects.iterator();
+ if (!iter.hasNext())
+ return;
+ RepoProject last = iter.next();
+ while (iter.hasNext()) {
+ RepoProject p = iter.next();
+ if (last.isAncestorOf(p))
+ iter.remove();
+ else
+ last = p;
+ }
+ removeNestedCopyAndLinkfiles();
+ }
+
+ private void removeNestedCopyAndLinkfiles() {
+ for (RepoProject proj : filteredProjects) {
+ List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
+ proj.clearCopyFiles();
+ for (CopyFile copyfile : copyfiles) {
+ if (!isNestedReferencefile(copyfile)) {
+ proj.addCopyFile(copyfile);
+ }
+ }
+ List<LinkFile> linkfiles = new ArrayList<>(proj.getLinkFiles());
+ proj.clearLinkFiles();
+ for (LinkFile linkfile : linkfiles) {
+ if (!isNestedReferencefile(linkfile)) {
+ proj.addLinkFile(linkfile);
+ }
+ }
+ }
+ }
+
+ boolean inGroups(RepoProject proj) {
+ for (String group : minusGroups) {
+ if (proj.inGroup(group)) {
+ // minus groups have highest priority.
+ return false;
+ }
+ }
+ if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
+ // empty plus groups means "all"
+ return true;
+ }
+ for (String group : plusGroups) {
+ if (proj.inGroup(group))
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isNestedReferencefile(ReferenceFile referencefile) {
+ if (referencefile.dest.indexOf('/') == -1) {
+ // If the referencefile is at root level then it won't be nested.
+ return false;
+ }
+ for (RepoProject proj : filteredProjects) {
+ if (proj.getPath().compareTo(referencefile.dest) > 0) {
+ // Early return as remaining projects can't be ancestor of this
+ // referencefile config (filteredProjects is sorted).
+ return false;
+ }
+ if (proj.isAncestorOf(referencefile.dest)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class Remote {
+ final String fetch;
+ final String revision;
+
+ Remote(String fetch, String revision) {
+ this.fetch = fetch;
+ this.revision = revision;
+ }
+ }
+}