Add support to <include> tag in repo manifest xml.

Change-Id: I32d468f92e24701ea680435bf3417e3850857303
Signed-off-by: Yuxuan 'fishy' Wang <fishywang@google.com>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index 3e5ef02..41a086f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -571,6 +571,46 @@ public void testRemoveOverlappingBare() throws Exception {
 		assertTrue("The a submodule should exist", a);
 	}
 
+	@Test
+	public void testIncludeTag() throws Exception {
+		Repository localDb = createWorkRepository();
+		Repository tempDb = createWorkRepository();
+
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+			.append("<manifest>")
+			.append("<include name=\"_include.xml\" />")
+			.append("<default revision=\"master\" remote=\"remote1\" />")
+			.append("</manifest>");
+		JGitTestUtil.writeTrashFile(
+				tempDb, "manifest.xml", xmlContent.toString());
+
+		xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+			.append("<manifest>")
+			.append("<remote name=\"remote1\" fetch=\".\" />")
+			.append("<default revision=\"master\" remote=\"remote1\" />")
+			.append("<project path=\"foo\" name=\"")
+			.append(defaultUri)
+			.append("\" />")
+			.append("</manifest>");
+		JGitTestUtil.writeTrashFile(
+				tempDb, "_include.xml", xmlContent.toString());
+
+		RepoCommand command = new RepoCommand(localDb);
+		command
+			.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+			.setURI(rootUri)
+			.call();
+		File hello = new File(localDb.getWorkTree(), "foo/hello.txt");
+		assertTrue("submodule should be checked out", hello.exists());
+		BufferedReader reader = new BufferedReader(new FileReader(hello));
+		String content = reader.readLine();
+		reader.close();
+		assertEquals("submodule content should be as expected",
+				"master world", content);
+	}
+
 	private void resolveRelativeUris() {
 		// Find the longest common prefix ends with "/" as rootUri.
 		defaultUri = defaultDb.getDirectory().toURI().toString();
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties
index 256dd7f..7443ad3 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/gitrepo/internal/RepoText.properties
@@ -1,4 +1,6 @@
 copyFileFailed=Error occurred during execution of copyfile rule.
+errorIncludeFile=Error: unable to read include file {0}
+errorIncludeNotImplemented=Error: <include> tag not supported as no callback defined.
 errorNoDefault=Error: no default remote in manifest file.
 errorNoDefaultFilename=Error: no default remote in manifest file {0}.
 errorParsingManifestFile=Error occurred during parsing manifest file.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
index 57514a2..52710d1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -123,6 +123,7 @@ public class RepoCommand extends GitCommand<RevCommit> {
 	private PersonIdent author;
 	private RemoteReader callback;
 	private InputStream inputStream;
+	private IncludedFileReader includedReader;
 
 	private List<Project> bareProjects;
 	private Git git;
@@ -163,6 +164,7 @@ public interface RemoteReader {
 		 * @return the file content.
 		 * @throws GitAPIException
 		 * @throws IOException
+		 * @since 3.5
 		 */
 		public byte[] readFile(String uri, String ref, String path)
 				throws GitAPIException, IOException;
@@ -224,6 +226,25 @@ public ObjectId sha1(String uri, String ref) throws GitAPIException {
 		}
 	}
 
+	/**
+	 * A callback to read included xml files.
+	 *
+	 * @since 3.5
+	 */
+	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
+		 * @throws IOException
+		 */
+		public InputStream readIncludeFile(String path)
+				throws GitAPIException, IOException;
+	}
+
 	private static class CopyFile {
 		final Repository repo;
 		final String path;
@@ -309,7 +330,6 @@ public int compareTo(Project that) {
 
 	private static class XmlManifest extends DefaultHandler {
 		private final RepoCommand command;
-		private final InputStream inputStream;
 		private final String filename;
 		private final String baseUrl;
 		private final Map<String, String> remotes;
@@ -318,12 +338,14 @@ private static class XmlManifest extends DefaultHandler {
 		private List<Project> projects;
 		private String defaultRemote;
 		private String defaultRevision;
+		private IncludedFileReader includedReader;
+		private int xmlInRead;
 		private Project currentProject;
 
-		XmlManifest(RepoCommand command, InputStream inputStream,
+		XmlManifest(RepoCommand command, IncludedFileReader includedReader,
 				String filename, String baseUrl, String groups) {
 			this.command = command;
-			this.inputStream = inputStream;
+			this.includedReader = includedReader;
 			this.filename = filename;
 
 			// Strip trailing /s to match repo behavior.
@@ -349,7 +371,8 @@ private static class XmlManifest extends DefaultHandler {
 			}
 		}
 
-		void read() throws IOException {
+		void read(InputStream inputStream) throws IOException {
+			xmlInRead++;
 			final XMLReader xr;
 			try {
 				xr = XMLReaderFactory.createXMLReader();
@@ -395,6 +418,35 @@ public void startElement(
 							currentProject.path,
 							attributes.getValue("src"), //$NON-NLS-1$
 							attributes.getValue("dest"))); //$NON-NLS-1$
+			} else if ("include".equals(qName)) { //$NON_NLS-1$
+				String name = attributes.getValue("name");
+				InputStream is = null;
+				if (includedReader != null) {
+					try {
+						is = includedReader.readIncludeFile(name);
+					} 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 {
+						is = new FileInputStream(path);
+					} catch (IOException e) {
+						throw new SAXException(MessageFormat.format(
+								RepoText.get().errorIncludeFile, path), e);
+					}
+				}
+				if (is == null) {
+					throw new SAXException(
+							RepoText.get().errorIncludeNotImplemented);
+				}
+				try {
+					read(is);
+				} catch (IOException e) {
+					throw new SAXException(e);
+				}
 			}
 		}
 
@@ -411,6 +463,10 @@ public void endElement(
 
 		@Override
 		public void endDocument() throws SAXException {
+			xmlInRead--;
+			if (xmlInRead != 0)
+				return;
+			// Only do the following after we finished reading everything.
 			if (defaultRemote == null) {
 				if (filename != null)
 					throw new SAXException(MessageFormat.format(
@@ -606,6 +662,17 @@ public RepoCommand setRemoteReader(final RemoteReader callback) {
 		return this;
 	}
 
+	/**
+	 * Set the IncludedFileReader callback.
+	 *
+	 * @param reader
+	 * @return this command
+	 */
+	public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
+		this.includedReader = reader;
+		return this;
+	}
+
 	@Override
 	public RevCommit call() throws GitAPIException {
 		try {
@@ -635,9 +702,9 @@ public RevCommit call() throws GitAPIException {
 				git = new Git(repo);
 
 			XmlManifest manifest = new XmlManifest(
-					this, inputStream, path, uri, groups);
+					this, includedReader, path, uri, groups);
 			try {
-				manifest.read();
+				manifest.read(inputStream);
 			} catch (IOException e) {
 				throw new ManifestErrorException(e);
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java
index 1f9d5d8..36b6e3a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java
@@ -60,6 +60,8 @@ public static RepoText get() {
 
 	// @formatter:off
 	/***/ public String copyFileFailed;
+	/***/ public String errorIncludeFile;
+	/***/ public String errorIncludeNotImplemented;
 	/***/ public String errorNoDefault;
 	/***/ public String errorNoDefaultFilename;
 	/***/ public String errorParsingManifestFile;