View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.gitrepo;
44  
45  import java.io.FileInputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.net.URI;
49  import java.net.URISyntaxException;
50  import java.text.MessageFormat;
51  import java.util.ArrayList;
52  import java.util.Collections;
53  import java.util.HashMap;
54  import java.util.HashSet;
55  import java.util.Iterator;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Set;
59  
60  import org.eclipse.jgit.annotations.NonNull;
61  import org.eclipse.jgit.api.errors.GitAPIException;
62  import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
63  import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
64  import org.eclipse.jgit.gitrepo.RepoProject.ReferenceFile;
65  import org.eclipse.jgit.gitrepo.internal.RepoText;
66  import org.eclipse.jgit.internal.JGitText;
67  import org.eclipse.jgit.lib.Repository;
68  import org.xml.sax.Attributes;
69  import org.xml.sax.InputSource;
70  import org.xml.sax.SAXException;
71  import org.xml.sax.XMLReader;
72  import org.xml.sax.helpers.DefaultHandler;
73  import org.xml.sax.helpers.XMLReaderFactory;
74  
75  /**
76   * Repo XML manifest parser.
77   *
78   * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
79   * @since 4.0
80   */
81  public class ManifestParser extends DefaultHandler {
82  	private final String filename;
83  	private final URI baseUrl;
84  	private final String defaultBranch;
85  	private final Repository rootRepo;
86  	private final Map<String, Remote> remotes;
87  	private final Set<String> plusGroups;
88  	private final Set<String> minusGroups;
89  	private final List<RepoProject> projects;
90  	private final List<RepoProject> filteredProjects;
91  	private final IncludedFileReader includedReader;
92  
93  	private String defaultRemote;
94  	private String defaultRevision;
95  	private int xmlInRead;
96  	private RepoProject currentProject;
97  
98  	/**
99  	 * A callback to read included xml files.
100 	 */
101 	public interface IncludedFileReader {
102 		/**
103 		 * Read a file from the same base dir of the manifest xml file.
104 		 *
105 		 * @param path
106 		 *            The relative path to the file to read
107 		 * @return the {@code InputStream} of the file.
108 		 * @throws GitAPIException
109 		 * @throws IOException
110 		 */
111 		public InputStream readIncludeFile(String path)
112 				throws GitAPIException, IOException;
113 	}
114 
115 	/**
116 	 * Constructor for ManifestParser
117 	 *
118 	 * @param includedReader
119 	 *            a
120 	 *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
121 	 *            object.
122 	 * @param filename
123 	 *            a {@link java.lang.String} object.
124 	 * @param defaultBranch
125 	 *            a {@link java.lang.String} object.
126 	 * @param baseUrl
127 	 *            a {@link java.lang.String} object.
128 	 * @param groups
129 	 *            a {@link java.lang.String} object.
130 	 * @param rootRepo
131 	 *            a {@link org.eclipse.jgit.lib.Repository} object.
132 	 */
133 	public ManifestParser(IncludedFileReader includedReader, String filename,
134 			String defaultBranch, String baseUrl, String groups,
135 			Repository rootRepo) {
136 		this.includedReader = includedReader;
137 		this.filename = filename;
138 		this.defaultBranch = defaultBranch;
139 		this.rootRepo = rootRepo;
140 		this.baseUrl = normalizeEmptyPath(URI.create(baseUrl));
141 
142 		plusGroups = new HashSet<>();
143 		minusGroups = new HashSet<>();
144 		if (groups == null || groups.length() == 0
145 				|| groups.equals("default")) { //$NON-NLS-1$
146 			// default means "all,-notdefault"
147 			minusGroups.add("notdefault"); //$NON-NLS-1$
148 		} else {
149 			for (String group : groups.split(",")) { //$NON-NLS-1$
150 				if (group.startsWith("-")) //$NON-NLS-1$
151 					minusGroups.add(group.substring(1));
152 				else
153 					plusGroups.add(group);
154 			}
155 		}
156 
157 		remotes = new HashMap<>();
158 		projects = new ArrayList<>();
159 		filteredProjects = new ArrayList<>();
160 	}
161 
162 	/**
163 	 * Read the xml file.
164 	 *
165 	 * @param inputStream
166 	 *            a {@link java.io.InputStream} object.
167 	 * @throws java.io.IOException
168 	 */
169 	public void read(InputStream inputStream) throws IOException {
170 		xmlInRead++;
171 		final XMLReader xr;
172 		try {
173 			xr = XMLReaderFactory.createXMLReader();
174 		} catch (SAXException e) {
175 			throw new IOException(JGitText.get().noXMLParserAvailable);
176 		}
177 		xr.setContentHandler(this);
178 		try {
179 			xr.parse(new InputSource(inputStream));
180 		} catch (SAXException e) {
181 			throw new IOException(RepoText.get().errorParsingManifestFile, e);
182 		}
183 	}
184 
185 	/** {@inheritDoc} */
186 	@Override
187 	public void startElement(
188 			String uri,
189 			String localName,
190 			String qName,
191 			Attributes attributes) throws SAXException {
192 		if ("project".equals(qName)) { //$NON-NLS-1$
193 			if (attributes.getValue("name") == null) { //$NON-NLS-1$
194 				throw new SAXException(RepoText.get().invalidManifest);
195 			}
196 			currentProject = new RepoProject(
197 					attributes.getValue("name"), //$NON-NLS-1$
198 					attributes.getValue("path"), //$NON-NLS-1$
199 					attributes.getValue("revision"), //$NON-NLS-1$
200 					attributes.getValue("remote"), //$NON-NLS-1$
201 					attributes.getValue("groups")); //$NON-NLS-1$
202 			currentProject.setRecommendShallow(
203 				attributes.getValue("clone-depth")); //$NON-NLS-1$
204 		} else if ("remote".equals(qName)) { //$NON-NLS-1$
205 			String alias = attributes.getValue("alias"); //$NON-NLS-1$
206 			String fetch = attributes.getValue("fetch"); //$NON-NLS-1$
207 			String revision = attributes.getValue("revision"); //$NON-NLS-1$
208 			Remote remote = new Remote(fetch, revision);
209 			remotes.put(attributes.getValue("name"), remote); //$NON-NLS-1$
210 			if (alias != null)
211 				remotes.put(alias, remote);
212 		} else if ("default".equals(qName)) { //$NON-NLS-1$
213 			defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
214 			defaultRevision = attributes.getValue("revision"); //$NON-NLS-1$
215 		} else if ("copyfile".equals(qName)) { //$NON-NLS-1$
216 			if (currentProject == null)
217 				throw new SAXException(RepoText.get().invalidManifest);
218 			currentProject.addCopyFile(new CopyFile(
219 						rootRepo,
220 						currentProject.getPath(),
221 						attributes.getValue("src"), //$NON-NLS-1$
222 						attributes.getValue("dest"))); //$NON-NLS-1$
223 		} else if ("linkfile".equals(qName)) { //$NON-NLS-1$
224 			if (currentProject == null) {
225 				throw new SAXException(RepoText.get().invalidManifest);
226 			}
227 			currentProject.addLinkFile(new LinkFile(
228 						rootRepo,
229 						currentProject.getPath(),
230 						attributes.getValue("src"), //$NON-NLS-1$
231 						attributes.getValue("dest"))); //$NON-NLS-1$
232 		} else if ("include".equals(qName)) { //$NON-NLS-1$
233 			String name = attributes.getValue("name"); //$NON-NLS-1$
234 			if (includedReader != null) {
235 				try (InputStream is = includedReader.readIncludeFile(name)) {
236 					if (is == null) {
237 						throw new SAXException(
238 								RepoText.get().errorIncludeNotImplemented);
239 					}
240 					read(is);
241 				} catch (Exception e) {
242 					throw new SAXException(MessageFormat.format(
243 							RepoText.get().errorIncludeFile, name), e);
244 				}
245 			} else if (filename != null) {
246 				int index = filename.lastIndexOf('/');
247 				String path = filename.substring(0, index + 1) + name;
248 				try (InputStream is = new FileInputStream(path)) {
249 					read(is);
250 				} catch (IOException e) {
251 					throw new SAXException(MessageFormat.format(
252 							RepoText.get().errorIncludeFile, path), e);
253 				}
254 			}
255 		}
256 	}
257 
258 	/** {@inheritDoc} */
259 	@Override
260 	public void endElement(
261 			String uri,
262 			String localName,
263 			String qName) throws SAXException {
264 		if ("project".equals(qName)) { //$NON-NLS-1$
265 			projects.add(currentProject);
266 			currentProject = null;
267 		}
268 	}
269 
270 	/** {@inheritDoc} */
271 	@Override
272 	public void endDocument() throws SAXException {
273 		xmlInRead--;
274 		if (xmlInRead != 0)
275 			return;
276 
277 		// Only do the following after we finished reading everything.
278 		Map<String, URI> remoteUrls = new HashMap<>();
279 		if (defaultRevision == null && defaultRemote != null) {
280 			Remote remote = remotes.get(defaultRemote);
281 			if (remote != null) {
282 				defaultRevision = remote.revision;
283 			}
284 			if (defaultRevision == null) {
285 				defaultRevision = defaultBranch;
286 			}
287 		}
288 		for (RepoProject proj : projects) {
289 			String remote = proj.getRemote();
290 			String revision = defaultRevision;
291 			if (remote == null) {
292 				if (defaultRemote == null) {
293 					if (filename != null)
294 						throw new SAXException(MessageFormat.format(
295 								RepoText.get().errorNoDefaultFilename,
296 								filename));
297 					else
298 						throw new SAXException(
299 								RepoText.get().errorNoDefault);
300 				}
301 				remote = defaultRemote;
302 			} else {
303 				Remote r = remotes.get(remote);
304 				if (r != null && r.revision != null) {
305 					revision = r.revision;
306 				}
307 			}
308 			URI remoteUrl = remoteUrls.get(remote);
309 			if (remoteUrl == null) {
310 				String fetch = remotes.get(remote).fetch;
311 				if (fetch == null) {
312 					throw new SAXException(MessageFormat
313 							.format(RepoText.get().errorNoFetch, remote));
314 				}
315 				remoteUrl = normalizeEmptyPath(baseUrl.resolve(fetch));
316 				remoteUrls.put(remote, remoteUrl);
317 			}
318 			proj.setUrl(remoteUrl.resolve(proj.getName()).toString())
319 				.setDefaultRevision(revision);
320 		}
321 
322 		filteredProjects.addAll(projects);
323 		removeNotInGroup();
324 		removeOverlaps();
325 	}
326 
327 	static URI normalizeEmptyPath(URI u) {
328 		// URI.create("scheme://host").resolve("a/b") => "scheme://hosta/b"
329 		// That seems like bug https://bugs.openjdk.java.net/browse/JDK-4666701.
330 		// We workaround this by special casing the empty path case.
331 		if (u.getHost() != null && !u.getHost().isEmpty() &&
332 			(u.getPath() == null || u.getPath().isEmpty())) {
333 			try {
334 				return new URI(u.getScheme(),
335 					u.getUserInfo(), u.getHost(), u.getPort(),
336 						"/", u.getQuery(), u.getFragment()); //$NON-NLS-1$
337 			} catch (URISyntaxException x) {
338 				throw new IllegalArgumentException(x.getMessage(), x);
339 			}
340 		}
341 		return u;
342 	}
343 
344 	/**
345 	 * Getter for projects.
346 	 *
347 	 * @return projects list reference, never null
348 	 */
349 	public List<RepoProject> getProjects() {
350 		return projects;
351 	}
352 
353 	/**
354 	 * Getter for filterdProjects.
355 	 *
356 	 * @return filtered projects list reference, never null
357 	 */
358 	public @NonNull List<RepoProject> getFilteredProjects() {
359 		return filteredProjects;
360 	}
361 
362 	/** Remove projects that are not in our desired groups. */
363 	void removeNotInGroup() {
364 		Iterator<RepoProject> iter = filteredProjects.iterator();
365 		while (iter.hasNext())
366 			if (!inGroups(iter.next()))
367 				iter.remove();
368 	}
369 
370 	/** Remove projects that sits in a subdirectory of any other project. */
371 	void removeOverlaps() {
372 		Collections.sort(filteredProjects);
373 		Iterator<RepoProject> iter = filteredProjects.iterator();
374 		if (!iter.hasNext())
375 			return;
376 		RepoProject last = iter.next();
377 		while (iter.hasNext()) {
378 			RepoProject p = iter.next();
379 			if (last.isAncestorOf(p))
380 				iter.remove();
381 			else
382 				last = p;
383 		}
384 		removeNestedCopyAndLinkfiles();
385 	}
386 
387 	private void removeNestedCopyAndLinkfiles() {
388 		for (RepoProject proj : filteredProjects) {
389 			List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
390 			proj.clearCopyFiles();
391 			for (CopyFile copyfile : copyfiles) {
392 				if (!isNestedReferencefile(copyfile)) {
393 					proj.addCopyFile(copyfile);
394 				}
395 			}
396 			List<LinkFile> linkfiles = new ArrayList<>(proj.getLinkFiles());
397 			proj.clearLinkFiles();
398 			for (LinkFile linkfile : linkfiles) {
399 				if (!isNestedReferencefile(linkfile)) {
400 					proj.addLinkFile(linkfile);
401 				}
402 			}
403 		}
404 	}
405 
406 	boolean inGroups(RepoProject proj) {
407 		for (String group : minusGroups) {
408 			if (proj.inGroup(group)) {
409 				// minus groups have highest priority.
410 				return false;
411 			}
412 		}
413 		if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
414 			// empty plus groups means "all"
415 			return true;
416 		}
417 		for (String group : plusGroups) {
418 			if (proj.inGroup(group))
419 				return true;
420 		}
421 		return false;
422 	}
423 
424 	private boolean isNestedReferencefile(ReferenceFile referencefile) {
425 		if (referencefile.dest.indexOf('/') == -1) {
426 			// If the referencefile is at root level then it won't be nested.
427 			return false;
428 		}
429 		for (RepoProject proj : filteredProjects) {
430 			if (proj.getPath().compareTo(referencefile.dest) > 0) {
431 				// Early return as remaining projects can't be ancestor of this
432 				// referencefile config (filteredProjects is sorted).
433 				return false;
434 			}
435 			if (proj.isAncestorOf(referencefile.dest)) {
436 				return true;
437 			}
438 		}
439 		return false;
440 	}
441 
442 	private static class Remote {
443 		final String fetch;
444 		final String revision;
445 
446 		Remote(String fetch, String revision) {
447 			this.fetch = fetch;
448 			this.revision = revision;
449 		}
450 	}
451 }