tikhomirov@36: /*
tikhomirov@532:  * Copyright (c) 2011-2013 TMate Software Ltd
tikhomirov@74:  *  
tikhomirov@74:  * This program is free software; you can redistribute it and/or modify
tikhomirov@74:  * it under the terms of the GNU General Public License as published by
tikhomirov@74:  * the Free Software Foundation; version 2 of the License.
tikhomirov@74:  *
tikhomirov@74:  * This program is distributed in the hope that it will be useful,
tikhomirov@74:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@74:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
tikhomirov@74:  * GNU General Public License for more details.
tikhomirov@74:  *
tikhomirov@74:  * For information on how to redistribute this software under
tikhomirov@74:  * the terms of a license other than GNU General Public License
tikhomirov@102:  * contact TMate Software at support@hg4j.com
tikhomirov@36:  */
tikhomirov@74: package org.tmatesoft.hg.repo;
tikhomirov@36: 
tikhomirov@36: import java.io.File;
tikhomirov@36: import java.io.IOException;
tikhomirov@512: import java.util.ConcurrentModificationException;
tikhomirov@36: 
tikhomirov@74: import org.tmatesoft.hg.core.Nodeid;
tikhomirov@357: import org.tmatesoft.hg.core.SessionContext;
tikhomirov@157: import org.tmatesoft.hg.internal.ByteArrayChannel;
tikhomirov@157: import org.tmatesoft.hg.internal.ByteArrayDataAccess;
tikhomirov@512: import org.tmatesoft.hg.internal.Callback;
tikhomirov@74: import org.tmatesoft.hg.internal.DataAccess;
tikhomirov@74: import org.tmatesoft.hg.internal.DataAccessProvider;
tikhomirov@74: import org.tmatesoft.hg.internal.DigestHelper;
tikhomirov@358: import org.tmatesoft.hg.internal.Experimental;
tikhomirov@169: import org.tmatesoft.hg.internal.InflaterDataAccess;
tikhomirov@526: import org.tmatesoft.hg.internal.Internals;
tikhomirov@512: import org.tmatesoft.hg.internal.Lifecycle;
tikhomirov@329: import org.tmatesoft.hg.internal.Patch;
tikhomirov@154: import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
tikhomirov@512: import org.tmatesoft.hg.util.Adaptable;
tikhomirov@157: import org.tmatesoft.hg.util.CancelledException;
tikhomirov@74: 
tikhomirov@36: /**
tikhomirov@423:  * WORK IN PROGRESS
tikhomirov@423:  * 
tikhomirov@36:  * @see http://mercurial.selenic.com/wiki/BundleFormat
tikhomirov@169:  * 
tikhomirov@74:  * @author Artem Tikhomirov
tikhomirov@74:  * @author TMate Software Ltd.
tikhomirov@36:  */
tikhomirov@467: @Experimental(reason="API is not stable")
tikhomirov@36: public class HgBundle {
tikhomirov@36: 
tikhomirov@36: 	private final File bundleFile;
tikhomirov@36: 	private final DataAccessProvider accessProvider;
tikhomirov@357: //	private final SessionContext sessionContext;
tikhomirov@512: 	private Lifecycle.BasicCallback flowControl;
tikhomirov@36: 
tikhomirov@357: 	HgBundle(SessionContext ctx, DataAccessProvider dap, File bundle) {
tikhomirov@357: //		sessionContext = ctx;
tikhomirov@36: 		accessProvider = dap;
tikhomirov@36: 		bundleFile = bundle;
tikhomirov@36: 	}
tikhomirov@36: 
tikhomirov@169: 	private DataAccess getDataStream() throws IOException {
tikhomirov@534: 		DataAccess da = accessProvider.createReader(bundleFile);
tikhomirov@169: 		byte[] signature = new byte[6];
tikhomirov@169: 		if (da.length() > 6) {
tikhomirov@169: 			da.readBytes(signature, 0, 6);
tikhomirov@169: 			if (signature[0] == 'H' && signature[1] == 'G' && signature[2] == '1' && signature[3] == '0') {
tikhomirov@169: 				if (signature[4] == 'G' && signature[5] == 'Z') {
tikhomirov@169: 					return new InflaterDataAccess(da, 6, da.length() - 6);
tikhomirov@169: 				}
tikhomirov@169: 				if (signature[4] == 'B' && signature[5] == 'Z') {
tikhomirov@526: 					throw Internals.notImplemented();
tikhomirov@169: 				}
tikhomirov@169: 				if (signature[4] != 'U' || signature[5] != 'N') {
tikhomirov@423: 					throw new HgInvalidStateException(String.format("Bad bundle signature: %s",  String.valueOf(signature)));
tikhomirov@169: 				}
tikhomirov@169: 				// "...UN", fall-through
tikhomirov@169: 			} else {
tikhomirov@169: 				da.reset();
tikhomirov@39: 			}
tikhomirov@169: 		}
tikhomirov@169: 		return da;
tikhomirov@169: 	}
tikhomirov@169: 
tikhomirov@186: 	private int uses = 0;
tikhomirov@186: 	public HgBundle link() {
tikhomirov@186: 		uses++;
tikhomirov@186: 		return this;
tikhomirov@186: 	}
tikhomirov@186: 	public void unlink() {
tikhomirov@186: 		uses--;
tikhomirov@186: 		if (uses == 0 && bundleFile != null) {
tikhomirov@186: 			bundleFile.deleteOnExit();
tikhomirov@186: 		}
tikhomirov@186: 	}
tikhomirov@186: 	public boolean inUse() {
tikhomirov@186: 		return uses > 0;
tikhomirov@186: 	}
tikhomirov@186: 
tikhomirov@182: 	/**
tikhomirov@182: 	 * Get changes recorded in the bundle that are missing from the supplied repository.
tikhomirov@182: 	 * @param hgRepo repository that shall possess base revision for this bundle
tikhomirov@182: 	 * @param inspector callback to get each changeset found 
tikhomirov@182: 	 */
tikhomirov@423: 	public void changes(final HgRepository hgRepo, final HgChangelog.Inspector inspector) throws HgRuntimeException {
tikhomirov@182: 		Inspector bundleInsp = new Inspector() {
tikhomirov@169: 			DigestHelper dh = new DigestHelper();
tikhomirov@169: 			boolean emptyChangelog = true;
tikhomirov@169: 			private DataAccess prevRevContent;
tikhomirov@182: 			private int revisionIndex;
tikhomirov@169: 
tikhomirov@169: 			public void changelogStart() {
tikhomirov@169: 				emptyChangelog = true;
tikhomirov@182: 				revisionIndex = 0;
tikhomirov@169: 			}
tikhomirov@169: 
tikhomirov@169: 			public void changelogEnd() {
tikhomirov@169: 				if (emptyChangelog) {
tikhomirov@169: 					throw new IllegalStateException("No changelog group in the bundle"); // XXX perhaps, just be silent and/or log?
tikhomirov@42: 				}
tikhomirov@37: 			}
tikhomirov@169: 
tikhomirov@169: /*
tikhomirov@169:  * Despite that BundleFormat wiki says: "Each Changelog entry patches the result of all previous patches 
tikhomirov@169:  * (the previous, or parent patch of a given patch p is the patch that has a node equal to p's p1 field)",
tikhomirov@169:  *  it seems not to hold true. Instead, each entry patches previous one, regardless of whether the one
tikhomirov@169:  *  before is its parent (i.e. ge.firstParent()) or not.
tikhomirov@169:  *  
tikhomirov@169: Actual state in the changelog.i
tikhomirov@169: Index    Offset      Flags     Packed     Actual   Base Rev   Link Rev  Parent1  Parent2     nodeid
tikhomirov@169:   50:          9212      0        209        329         48         50       49       -1     f1db8610da62a3e0beb8d360556ee1fd6eb9885e
tikhomirov@169:   51:          9421      0        278        688         48         51       50       -1     9429c7bd1920fab164a9d2b621d38d57bcb49ae0
tikhomirov@169:   52:          9699      0        154        179         52         52       50       -1     30bd389788464287cee22ccff54c330a4b715de5
tikhomirov@169:   53:          9853      0        133        204         52         53       51       52     a6f39e595b2b54f56304470269a936ead77f5725
tikhomirov@169:   54:          9986      0        156        182         54         54       52       -1     fd4f2c98995beb051070630c272a9be87bef617d
tikhomirov@169: 
tikhomirov@169: Excerpt from bundle (nodeid, p1, p2, cs):
tikhomirov@169:    f1db8610da62a3e0beb8d360556ee1fd6eb9885e 26e3eeaa39623de552b45ee1f55c14f36460f220 0000000000000000000000000000000000000000 f1db8610da62a3e0beb8d360556ee1fd6eb9885e; patches:4
tikhomirov@169:    9429c7bd1920fab164a9d2b621d38d57bcb49ae0 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 9429c7bd1920fab164a9d2b621d38d57bcb49ae0; patches:3
tikhomirov@169: >  30bd389788464287cee22ccff54c330a4b715de5 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 30bd389788464287cee22ccff54c330a4b715de5; patches:3
tikhomirov@169:    a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 a6f39e595b2b54f56304470269a936ead77f5725; patches:3
tikhomirov@169:    fd4f2c98995beb051070630c272a9be87bef617d 30bd389788464287cee22ccff54c330a4b715de5 0000000000000000000000000000000000000000 fd4f2c98995beb051070630c272a9be87bef617d; patches:3
tikhomirov@169: 
tikhomirov@169: To recreate 30bd..e5, one have to take content of 9429..e0, not its p1 f1db..5e
tikhomirov@169:  */
tikhomirov@169: 			public boolean element(GroupElement ge) {
tikhomirov@169: 				emptyChangelog = false;
tikhomirov@169: 				HgChangelog changelog = hgRepo.getChangelog();
tikhomirov@169: 				try {
tikhomirov@169: 					if (prevRevContent == null) { 
tikhomirov@274: 						if (ge.firstParent().isNull() && ge.secondParent().isNull()) {
tikhomirov@169: 							prevRevContent = new ByteArrayDataAccess(new byte[0]);
tikhomirov@169: 						} else {
tikhomirov@169: 							final Nodeid base = ge.firstParent();
tikhomirov@169: 							if (!changelog.isKnown(base) /*only first parent, that's Bundle contract*/) {
tikhomirov@169: 								throw new IllegalStateException(String.format("Revision %s needs a parent %s, which is missing in the supplied repo %s", ge.node().shortNotation(), base.shortNotation(), hgRepo.toString()));
tikhomirov@169: 							}
tikhomirov@169: 							ByteArrayChannel bac = new ByteArrayChannel();
tikhomirov@418: 							changelog.rawContent(base, bac); // TODO post-1.0 get DataAccess directly, to avoid
tikhomirov@169: 							// extra byte[] (inside ByteArrayChannel) duplication just for the sake of subsequent ByteArrayDataChannel wrap.
tikhomirov@169: 							prevRevContent = new ByteArrayDataAccess(bac.toArray());
tikhomirov@169: 						}
tikhomirov@169: 					}
tikhomirov@169: 					//
tikhomirov@169: 					byte[] csetContent = ge.apply(prevRevContent);
tikhomirov@169: 					dh = dh.sha1(ge.firstParent(), ge.secondParent(), csetContent); // XXX ge may give me access to byte[] content of nodeid directly, perhaps, I don't need DH to be friend of Nodeid?
tikhomirov@169: 					if (!ge.node().equalsTo(dh.asBinary())) {
tikhomirov@423: 						throw new HgInvalidStateException(String.format("Integrity check failed on %s, node: %s", bundleFile, ge.node().shortNotation()));
tikhomirov@169: 					}
tikhomirov@169: 					ByteArrayDataAccess csetDataAccess = new ByteArrayDataAccess(csetContent);
tikhomirov@169: 					RawChangeset cs = RawChangeset.parse(csetDataAccess);
tikhomirov@182: 					inspector.next(revisionIndex++, ge.node(), cs);
tikhomirov@170: 					prevRevContent.done();
tikhomirov@169: 					prevRevContent = csetDataAccess.reset();
tikhomirov@169: 				} catch (CancelledException ex) {
tikhomirov@169: 					return false;
tikhomirov@423: 				} catch (IOException ex) {
tikhomirov@423: 					throw new HgInvalidFileException("Invalid bundle file", ex, bundleFile); // TODO post-1.0 revisit exception handling
tikhomirov@427: 				} catch (HgInvalidDataFormatException ex) {
tikhomirov@423: 					throw new HgInvalidControlFileException("Invalid bundle file", ex, bundleFile);
tikhomirov@169: 				}
tikhomirov@169: 				return true;
tikhomirov@169: 			}
tikhomirov@169: 
tikhomirov@169: 			public void manifestStart() {}
tikhomirov@169: 			public void manifestEnd() {}
tikhomirov@169: 			public void fileStart(String name) {}
tikhomirov@169: 			public void fileEnd(String name) {}
tikhomirov@169: 
tikhomirov@169: 		};
tikhomirov@423: 		inspectChangelog(bundleInsp);
tikhomirov@169: 	}
tikhomirov@169: 
tikhomirov@169: 	// callback to minimize amount of Strings and Nodeids instantiated
tikhomirov@512: 	@Callback
tikhomirov@169: 	public interface Inspector {
tikhomirov@169: 		void changelogStart();
tikhomirov@169: 
tikhomirov@169: 		void changelogEnd();
tikhomirov@169: 
tikhomirov@169: 		void manifestStart();
tikhomirov@169: 
tikhomirov@169: 		void manifestEnd();
tikhomirov@169: 
tikhomirov@169: 		void fileStart(String name);
tikhomirov@169: 
tikhomirov@169: 		void fileEnd(String name);
tikhomirov@169: 
tikhomirov@169: 		/**
tikhomirov@170: 		 * XXX desperately need exceptions here
tikhomirov@170: 		 * @param element data element, instance might be reused, don't keep a reference to it or its raw data
tikhomirov@169: 		 * @return true to continue
tikhomirov@169: 		 */
tikhomirov@169: 		boolean element(GroupElement element);
tikhomirov@169: 	}
tikhomirov@169: 
tikhomirov@423: 	/**
tikhomirov@423: 	 * @param inspector callback to visit changelog entries
tikhomirov@423: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@423: 	 * @throws IllegalArgumentException if inspector argument is null
tikhomirov@423: 	 */
tikhomirov@423: 	public void inspectChangelog(Inspector inspector) throws HgRuntimeException {
tikhomirov@169: 		if (inspector == null) {
tikhomirov@169: 			throw new IllegalArgumentException();
tikhomirov@169: 		}
tikhomirov@512: 		final Lifecycle lifecycle = lifecycleSetUp(inspector);
tikhomirov@295: 		DataAccess da = null;
tikhomirov@169: 		try {
tikhomirov@295: 			da = getDataStream();
tikhomirov@182: 			internalInspectChangelog(da, inspector);
tikhomirov@295: 		} catch (IOException ex) {
tikhomirov@295: 			throw new HgInvalidFileException("Bundle.inspectChangelog failed", ex, bundleFile);
tikhomirov@37: 		} finally {
tikhomirov@295: 			if (da != null) {
tikhomirov@295: 				da.done();
tikhomirov@295: 			}
tikhomirov@512: 			lifecycleTearDown(lifecycle);
tikhomirov@37: 		}
tikhomirov@37: 	}
tikhomirov@37: 
tikhomirov@423: 	/**
tikhomirov@423: 	 * @param inspector callback to visit manifest entries
tikhomirov@423: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@423: 	 * @throws IllegalArgumentException if inspector argument is null
tikhomirov@423: 	 */
tikhomirov@423: 	public void inspectManifest(Inspector inspector) throws HgRuntimeException {
tikhomirov@169: 		if (inspector == null) {
tikhomirov@169: 			throw new IllegalArgumentException();
tikhomirov@169: 		}
tikhomirov@512: 		final Lifecycle lifecycle = lifecycleSetUp(inspector);
tikhomirov@295: 		DataAccess da = null;
tikhomirov@36: 		try {
tikhomirov@295: 			da = getDataStream();
tikhomirov@169: 			if (da.isEmpty()) {
tikhomirov@169: 				return;
tikhomirov@169: 			}
tikhomirov@169: 			skipGroup(da); // changelog
tikhomirov@182: 			internalInspectManifest(da, inspector);
tikhomirov@295: 		} catch (IOException ex) {
tikhomirov@295: 			throw new HgInvalidFileException("Bundle.inspectManifest failed", ex, bundleFile);
tikhomirov@36: 		} finally {
tikhomirov@295: 			if (da != null) {
tikhomirov@295: 				da.done();
tikhomirov@295: 			}
tikhomirov@512: 			lifecycleTearDown(lifecycle);
tikhomirov@36: 		}
tikhomirov@36: 	}
tikhomirov@36: 
tikhomirov@423: 	/**
tikhomirov@423: 	 * @param inspector callback to visit file entries
tikhomirov@423: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@423: 	 * @throws IllegalArgumentException if inspector argument is null
tikhomirov@423: 	 */
tikhomirov@423: 	public void inspectFiles(Inspector inspector) throws HgRuntimeException {
tikhomirov@169: 		if (inspector == null) {
tikhomirov@169: 			throw new IllegalArgumentException();
tikhomirov@169: 		}
tikhomirov@512: 		final Lifecycle lifecycle = lifecycleSetUp(inspector);
tikhomirov@295: 		DataAccess da = null;
tikhomirov@169: 		try {
tikhomirov@295: 			da = getDataStream();
tikhomirov@182: 			if (da.isEmpty()) {
tikhomirov@182: 				return;
tikhomirov@169: 			}
tikhomirov@182: 			skipGroup(da); // changelog
tikhomirov@182: 			if (da.isEmpty()) {
tikhomirov@182: 				return;
tikhomirov@169: 			}
tikhomirov@182: 			skipGroup(da); // manifest
tikhomirov@182: 			internalInspectFiles(da, inspector);
tikhomirov@295: 		} catch (IOException ex) {
tikhomirov@295: 			throw new HgInvalidFileException("Bundle.inspectFiles failed", ex, bundleFile);
tikhomirov@169: 		} finally {
tikhomirov@295: 			if (da != null) {
tikhomirov@295: 				da.done();
tikhomirov@295: 			}
tikhomirov@512: 			lifecycleTearDown(lifecycle);
tikhomirov@169: 		}
tikhomirov@169: 	}
tikhomirov@169: 
tikhomirov@423: 	/**
tikhomirov@423: 	 * @param inspector visit complete bundle (changelog, manifest and file entries)
tikhomirov@423: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@423: 	 * @throws IllegalArgumentException if inspector argument is null
tikhomirov@423: 	 */
tikhomirov@423: 	public void inspectAll(Inspector inspector) throws HgRuntimeException {
tikhomirov@169: 		if (inspector == null) {
tikhomirov@169: 			throw new IllegalArgumentException();
tikhomirov@169: 		}
tikhomirov@512: 		final Lifecycle lifecycle = lifecycleSetUp(inspector);
tikhomirov@295: 		DataAccess da = null;
tikhomirov@169: 		try {
tikhomirov@295: 			da = getDataStream();
tikhomirov@182: 			internalInspectChangelog(da, inspector);
tikhomirov@513: 			if (flowControl.isStopped()) {
tikhomirov@513: 				return;
tikhomirov@513: 			}
tikhomirov@182: 			internalInspectManifest(da, inspector);
tikhomirov@513: 			if (flowControl.isStopped()) {
tikhomirov@513: 				return;
tikhomirov@513: 			}
tikhomirov@182: 			internalInspectFiles(da, inspector);
tikhomirov@295: 		} catch (IOException ex) {
tikhomirov@295: 			throw new HgInvalidFileException("Bundle.inspectAll failed", ex, bundleFile);
tikhomirov@169: 		} finally {
tikhomirov@295: 			if (da != null) {
tikhomirov@295: 				da.done();
tikhomirov@295: 			}
tikhomirov@512: 			lifecycleTearDown(lifecycle);
tikhomirov@169: 		}
tikhomirov@169: 	}
tikhomirov@512: 	
tikhomirov@512: 	// initialize flowControl, check for concurrent usage, starts lifecyle, if any
tikhomirov@512: 	// return non-null only if inspector is interested in lifecycle events
tikhomirov@512: 	private Lifecycle lifecycleSetUp(Inspector inspector) throws ConcurrentModificationException {
tikhomirov@512: 		// Don't need flowControl in case Inspector doesn't implement Lifecycle,
tikhomirov@512: 		// however is handy not to expect it == null inside internalInspect* 
tikhomirov@512: 		// XXX Once there's need to make this class thread-safe,
tikhomirov@512: 		// shall move flowControl to thread-local state.
tikhomirov@512: 		if (flowControl != null) {
tikhomirov@512: 			throw new ConcurrentModificationException("HgBundle is in use and not thread-safe yet");
tikhomirov@512: 		}
tikhomirov@512: 		flowControl = new Lifecycle.BasicCallback();
tikhomirov@512: 		final Lifecycle lifecycle = Adaptable.Factory.getAdapter(inspector, Lifecycle.class, null);
tikhomirov@512: 		if (lifecycle != null) {
tikhomirov@512: 			lifecycle.start(-1, flowControl, flowControl);
tikhomirov@512: 		}
tikhomirov@512: 		return lifecycle;
tikhomirov@512: 	}
tikhomirov@512: 	
tikhomirov@512: 	private void lifecycleTearDown(Lifecycle lifecycle) {
tikhomirov@512: 		if (lifecycle != null) {
tikhomirov@512: 			lifecycle.finish(flowControl);
tikhomirov@512: 		}
tikhomirov@512: 		flowControl = null;
tikhomirov@512: 	}
tikhomirov@169: 
tikhomirov@182: 	private void internalInspectChangelog(DataAccess da, Inspector inspector) throws IOException {
tikhomirov@182: 		if (da.isEmpty()) {
tikhomirov@182: 			return;
tikhomirov@182: 		}
tikhomirov@182: 		inspector.changelogStart();
tikhomirov@513: 		if (flowControl.isStopped()) {
tikhomirov@513: 			return;
tikhomirov@513: 		}
tikhomirov@182: 		readGroup(da, inspector);
tikhomirov@513: 		if (flowControl.isStopped()) {
tikhomirov@513: 			return;
tikhomirov@513: 		}
tikhomirov@182: 		inspector.changelogEnd();
tikhomirov@182: 	}
tikhomirov@182: 
tikhomirov@182: 	private void internalInspectManifest(DataAccess da, Inspector inspector) throws IOException {
tikhomirov@182: 		if (da.isEmpty()) {
tikhomirov@182: 			return;
tikhomirov@182: 		}
tikhomirov@182: 		inspector.manifestStart();
tikhomirov@513: 		if (flowControl.isStopped()) {
tikhomirov@513: 			return;
tikhomirov@513: 		}
tikhomirov@182: 		readGroup(da, inspector);
tikhomirov@513: 		if (flowControl.isStopped()) {
tikhomirov@513: 			return;
tikhomirov@513: 		}
tikhomirov@182: 		inspector.manifestEnd();
tikhomirov@182: 	}
tikhomirov@182: 
tikhomirov@182: 	private void internalInspectFiles(DataAccess da, Inspector inspector) throws IOException {
tikhomirov@182: 		while (!da.isEmpty()) {
tikhomirov@182: 			int fnameLen = da.readInt();
tikhomirov@182: 			if (fnameLen <= 4) {
tikhomirov@182: 				break; // null chunk, the last one.
tikhomirov@182: 			}
tikhomirov@182: 			byte[] fnameBuf = new byte[fnameLen - 4];
tikhomirov@182: 			da.readBytes(fnameBuf, 0, fnameBuf.length);
tikhomirov@182: 			String name = new String(fnameBuf);
tikhomirov@182: 			inspector.fileStart(name);
tikhomirov@513: 			if (flowControl.isStopped()) {
tikhomirov@513: 				return;
tikhomirov@513: 			}
tikhomirov@182: 			readGroup(da, inspector);
tikhomirov@513: 			if (flowControl.isStopped()) {
tikhomirov@513: 				return;
tikhomirov@513: 			}
tikhomirov@182: 			inspector.fileEnd(name);
tikhomirov@182: 		}
tikhomirov@182: 	}
tikhomirov@182: 
tikhomirov@169: 	private static void readGroup(DataAccess da, Inspector inspector) throws IOException {
tikhomirov@36: 		int len = da.readInt();
tikhomirov@169: 		boolean good2go = true;
tikhomirov@532: 		Nodeid prevNodeid = Nodeid.NULL;
tikhomirov@169: 		while (len > 4 && !da.isEmpty() && good2go) {
tikhomirov@36: 			byte[] nb = new byte[80];
tikhomirov@36: 			da.readBytes(nb, 0, 80);
tikhomirov@169: 			int dataLength = len - 84 /* length field + 4 nodeids */;
tikhomirov@169: 			byte[] data = new byte[dataLength];
tikhomirov@169: 			da.readBytes(data, 0, dataLength);
tikhomirov@169: 			DataAccess slice = new ByteArrayDataAccess(data); // XXX in fact, may pass a slicing DataAccess.
tikhomirov@169: 			// Just need to make sure that we seek to proper location afterwards (where next GroupElement starts),
tikhomirov@169: 			// regardless whether that slice has read it or not.
tikhomirov@532: 			GroupElement ge = new GroupElement(nb, prevNodeid, slice);
tikhomirov@169: 			good2go = inspector.element(ge);
tikhomirov@170: 			slice.done(); // BADA doesn't implement done(), but it could (e.g. free array) 
tikhomirov@170: 			/// and we'd better tell it we are not going to use it any more. However, it's important to ensure Inspector
tikhomirov@170: 			// implementations out there do not retain GroupElement.rawData()
tikhomirov@532: 			prevNodeid = ge.node();
tikhomirov@36: 			len = da.isEmpty() ? 0 : da.readInt();
tikhomirov@36: 		}
tikhomirov@169: 		// need to skip up to group end if inspector told he don't want to continue with the group, 
tikhomirov@169: 		// because outer code may try to read next group immediately as we return back.
tikhomirov@169: 		while (len > 4 && !da.isEmpty()) {
tikhomirov@169: 			da.skip(len - 4 /* length field */);
tikhomirov@169: 			len = da.isEmpty() ? 0 : da.readInt();
tikhomirov@169: 		}
tikhomirov@36: 	}
tikhomirov@36: 
tikhomirov@169: 	private static void skipGroup(DataAccess da) throws IOException {
tikhomirov@169: 		int len = da.readInt();
tikhomirov@169: 		while (len > 4 && !da.isEmpty()) {
tikhomirov@169: 			da.skip(len - 4); // sizeof(int)
tikhomirov@169: 			len = da.isEmpty() ? 0 : da.readInt();
tikhomirov@169: 		}
tikhomirov@169: 	}
tikhomirov@169: 
tikhomirov@358: 	@Experimental(reason="Cumbersome API, rawData and apply with byte[] perhaps need replacement with ByteChannel/ByteBuffer, and better Exceptions. Perhaps, shall split into interface and impl")
tikhomirov@169: 	public static class GroupElement {
tikhomirov@169: 		private final byte[] header; // byte[80] takes 120 bytes, 4 Nodeids - 192
tikhomirov@169: 		private final DataAccess dataAccess;
tikhomirov@329: 		private Patch patches;
tikhomirov@532: 		private final Nodeid deltaBase;
tikhomirov@169: 
tikhomirov@532: 		GroupElement(byte[] fourNodeids, Nodeid deltaBaseRev, DataAccess rawDataAccess) {
tikhomirov@36: 			assert fourNodeids != null && fourNodeids.length == 80;
tikhomirov@36: 			header = fourNodeids;
tikhomirov@532: 			deltaBase = deltaBaseRev;
tikhomirov@169: 			dataAccess = rawDataAccess;
tikhomirov@36: 		}
tikhomirov@169: 
tikhomirov@358: 		/**
tikhomirov@358: 		 * node field of the group element
tikhomirov@358: 		 * @return node revision, never null
tikhomirov@358: 		 */
tikhomirov@36: 		public Nodeid node() {
tikhomirov@36: 			return Nodeid.fromBinary(header, 0);
tikhomirov@36: 		}
tikhomirov@169: 
tikhomirov@358: 		/**
tikhomirov@358: 		 * p1 (parent 1) field of the group element
tikhomirov@358: 		 * @return revision of parent 1, never null
tikhomirov@358: 		 */
tikhomirov@36: 		public Nodeid firstParent() {
tikhomirov@36: 			return Nodeid.fromBinary(header, 20);
tikhomirov@36: 		}
tikhomirov@169: 
tikhomirov@358: 		/**
tikhomirov@358: 		 * p2 (parent 2) field of the group element
tikhomirov@358: 		 * @return revision of parent 2, never null
tikhomirov@358: 		 */
tikhomirov@36: 		public Nodeid secondParent() {
tikhomirov@36: 			return Nodeid.fromBinary(header, 40);
tikhomirov@36: 		}
tikhomirov@169: 
tikhomirov@358: 		/**
tikhomirov@358: 		 * cs (changeset link) field of the group element
tikhomirov@358: 		 * @return changeset revision, never null
tikhomirov@358: 		 */
tikhomirov@358: 		public Nodeid cset() {
tikhomirov@36: 			return Nodeid.fromBinary(header, 60);
tikhomirov@36: 		}
tikhomirov@358: 		
tikhomirov@532: 		/**
tikhomirov@532: 		 * Revision this element keeps patches against. For the patches of the very first revision returns {@link Nodeid#NULL}.
tikhomirov@532: 		 * @return revision of delta base, never null
tikhomirov@532: 		 */
tikhomirov@532: 		public Nodeid patchBase() {
tikhomirov@532: 			return deltaBase;
tikhomirov@532: 		}
tikhomirov@532: 		
tikhomirov@358: 		public byte[] rawDataByteArray() throws IOException { // XXX IOException or HgInvalidFileException?
tikhomirov@358: 			return rawData().byteArray();
tikhomirov@358: 		}
tikhomirov@358: 		
tikhomirov@358: 		public byte[] apply(byte[] baseContent) throws IOException {
tikhomirov@358: 			return apply(new ByteArrayDataAccess(baseContent));
tikhomirov@358: 		}
tikhomirov@169: 
tikhomirov@358: 		/*package-local*/ DataAccess rawData() {
tikhomirov@169: 			return dataAccess;
tikhomirov@169: 		}
tikhomirov@169: 		
tikhomirov@329: 		/*package-local*/ Patch patch() throws IOException {
tikhomirov@169: 			if (patches == null) {
tikhomirov@169: 				dataAccess.reset();
tikhomirov@329: 				patches = new Patch();
tikhomirov@329: 				patches.read(dataAccess);
tikhomirov@169: 			}
tikhomirov@169: 			return patches;
tikhomirov@169: 		}
tikhomirov@169: 
tikhomirov@358: 		/*package-local*/ byte[] apply(DataAccess baseContent) throws IOException {
tikhomirov@329: 			return patch().apply(baseContent, -1);
tikhomirov@169: 		}
tikhomirov@357: 		
tikhomirov@357: 		public String toString() {
tikhomirov@357: 			int patchCount;
tikhomirov@357: 			try {
tikhomirov@357: 				patchCount = patch().count();
tikhomirov@357: 			} catch (IOException ex) {
tikhomirov@357: 				ex.printStackTrace();
tikhomirov@357: 				patchCount = -1;
tikhomirov@357: 			}
tikhomirov@357: 			return String.format("%s %s %s %s; patches:%d\n", node().shortNotation(), firstParent().shortNotation(), secondParent().shortNotation(), cset().shortNotation(), patchCount);
tikhomirov@357: 		}
tikhomirov@36: 	}
tikhomirov@36: }