diff options
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.java | 449 |
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; + } + } +} |