Mercurial > hg4j
diff src/org/tmatesoft/hg/repo/HgDataFile.java @ 157:d5268ca7715b
Merged branch wrap-data-access into default for resource-friendly data access. Updated API to promote that friendliness to clients (channels, not byte[]). More exceptions
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Wed, 09 Mar 2011 05:22:17 +0100 |
parents | src/com/tmate/hgkit/ll/HgDataFile.java@9429c7bd1920 src/com/tmate/hgkit/ll/HgDataFile.java@1a7a9a20e1f9 |
children | b413b16d10a5 |
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java Wed Mar 02 01:06:09 2011 +0100 +++ b/src/org/tmatesoft/hg/repo/HgDataFile.java Wed Mar 09 05:22:17 2011 +0100 @@ -18,9 +18,8 @@ import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; import static org.tmatesoft.hg.repo.HgRepository.*; -import static org.tmatesoft.hg.repo.HgRepository.TIP; -import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -28,14 +27,14 @@ import java.util.TreeMap; import org.tmatesoft.hg.core.HgDataStreamException; +import org.tmatesoft.hg.core.HgException; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.FilterByteChannel; import org.tmatesoft.hg.internal.RevlogStream; import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.ProgressSupport; @@ -71,106 +70,86 @@ // human-readable (i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i") public Path getPath() { - return path; // hgRepo.backresolve(this) -> name? + return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? } public int length(Nodeid nodeid) { return content.dataLength(getLocalRevision(nodeid)); } - public byte[] content() { - return content(TIP); + public void workingCopy(ByteChannel sink) throws IOException, CancelledException { + throw HgRepository.notImplemented(); } - /*XXX not sure applyFilters is the best way to do, perhaps, callers shall add filters themselves?*/ - public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { - byte[] content = content(revision); - final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); - final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); - ByteBuffer buf = ByteBuffer.allocate(512); - int left = content.length; - progressSupport.start(left); - int offset = 0; - cancelSupport.checkCancelled(); - ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; - do { - buf.put(content, offset, Math.min(left, buf.remaining())); - buf.flip(); - cancelSupport.checkCancelled(); - // XXX I may not rely on returned number of bytes but track change in buf position instead. - int consumed = _sink.write(buf); - buf.compact(); - offset += consumed; - left -= consumed; - progressSupport.worked(consumed); - } while (left > 0); - progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. +// public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { +// byte[] content = content(revision); +// final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); +// final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); +// ByteBuffer buf = ByteBuffer.allocate(512); +// int left = content.length; +// progressSupport.start(left); +// int offset = 0; +// cancelSupport.checkCancelled(); +// ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; +// do { +// buf.put(content, offset, Math.min(left, buf.remaining())); +// buf.flip(); +// cancelSupport.checkCancelled(); +// // XXX I may not rely on returned number of bytes but track change in buf position instead. +// int consumed = _sink.write(buf); +// buf.compact(); +// offset += consumed; +// left -= consumed; +// progressSupport.worked(consumed); +// } while (left > 0); +// progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. +// } + + /*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/ + public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { + content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); } // for data files need to check heading of the file content for possible metadata // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- - @Override - public byte[] content(int revision) { + public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { if (revision == TIP) { revision = getLastRevision(); } - if (wrongLocalRevision(revision) || revision == BAD_REVISION || revision == WORKING_COPY) { + if (revision == WORKING_COPY) { + workingCopy(sink); + return; + } + if (wrongLocalRevision(revision) || revision == BAD_REVISION) { throw new IllegalArgumentException(String.valueOf(revision)); } - byte[] data = super.content(revision); + if (sink == null) { + throw new IllegalArgumentException(); + } if (metadata == null) { metadata = new Metadata(); } + ContentPipe insp; if (metadata.none(revision)) { - // although not very reasonable when data is byte array, this check might - // get handy when there's a stream/channel to avoid useless reads and rewinds. - return data; + insp = new ContentPipe(sink, 0); + } else if (metadata.known(revision)) { + insp = new ContentPipe(sink, metadata.dataOffset(revision)); + } else { + // do not know if there's metadata + insp = new MetadataContentPipe(sink, metadata); } - int toSkip = 0; - if (!metadata.known(revision)) { - if (data.length < 4 || (data[0] != 1 && data[1] != 10)) { - metadata.recordNone(revision); - return data; - } - int lastEntryStart = 2; - int lastColon = -1; - ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>(); - String key = null, value = null; - for (int i = 2; i < data.length; i++) { - if (data[i] == (int) ':') { - key = new String(data, lastEntryStart, i - lastEntryStart); - lastColon = i; - } else if (data[i] == '\n') { - if (key == null || lastColon == -1 || i <= lastColon) { - throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev - } - value = new String(data, lastColon + 1, i - lastColon - 1).trim(); - _metadata.add(new MetadataEntry(key, value)); - key = value = null; - lastColon = -1; - lastEntryStart = i+1; - } else if (data[i] == 1 && i + 1 < data.length && data[i+1] == 10) { - if (key != null && lastColon != -1 && i > lastColon) { - // just in case last entry didn't end with newline - value = new String(data, lastColon + 1, i - lastColon - 1); - _metadata.add(new MetadataEntry(key, value)); - } - lastEntryStart = i+1; - break; - } - } - _metadata.trimToSize(); - metadata.add(revision, lastEntryStart, _metadata); - toSkip = lastEntryStart; - } else { - toSkip = metadata.dataOffset(revision); + insp.checkCancelled(); + super.content.iterate(revision, revision, true, insp); + try { + insp.checkFailed(); + } catch (HgDataStreamException ex) { + throw ex; + } catch (HgException ex) { + // shall not happen, unless we changed ContentPipe or its subclass + throw new HgDataStreamException(ex.getClass().getName(), ex); } - // XXX copy of an array may be memory-hostile, a wrapper with baseOffsetShift(lastEntryStart) would be more convenient - byte[] rv = new byte[data.length - toSkip]; - System.arraycopy(data, toSkip, rv, 0, rv.length); - return rv; } - + public void history(HgChangelog.Inspector inspector) { history(0, getLastRevision(), inspector); } @@ -192,7 +171,7 @@ RevlogStream.Inspector insp = new RevlogStream.Inspector() { int count = 0; - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, byte[] data) { + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { commitRevisions[count++] = linkRevision; } }; @@ -210,10 +189,22 @@ return getRepo().getChangelog().getRevision(changelogRevision); } - public boolean isCopy() { + public boolean isCopy() throws HgDataStreamException { if (metadata == null || !metadata.checked(0)) { // content() always initializes metadata. - content(0); // FIXME expensive way to find out metadata, distinct RevlogStream.Iterator would be better. + // FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. + try { + content(0, new ByteChannel() { // No-op channel + public int write(ByteBuffer buffer) throws IOException { + // pretend we consumed whole buffer + int rv = buffer.remaining(); + buffer.position(buffer.limit()); + return rv; + } + }); + } catch (Exception ex) { + throw new HgDataStreamException("Can't initialize metadata", ex); + } } if (!metadata.known(0)) { return false; @@ -221,14 +212,14 @@ return metadata.find(0, "copy") != null; } - public Path getCopySourceName() { + public Path getCopySourceName() throws HgDataStreamException { if (isCopy()) { return Path.create(metadata.find(0, "copy")); } throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?) } - public Nodeid getCopySourceRevision() { + public Nodeid getCopySourceRevision() throws HgDataStreamException { if (isCopy()) { return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid } @@ -317,4 +308,76 @@ return null; } } + + private static class MetadataContentPipe extends ContentPipe { + + private final Metadata metadata; + + public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { + super(sink, 0); + metadata = _metadata; + } + + @Override + protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { + long daLength = da.length(); + if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { + metadata.recordNone(revisionNumber); + da.reset(); + return; + } + int lastEntryStart = 2; + int lastColon = -1; + ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>(); + // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, + // which can't be used here because we can't convert bytes to chars as we read them + // (there might be multi-byte encoding), and we need to collect all bytes before converting to string + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String key = null, value = null; + boolean byteOne = false; + for (int i = 2; i < daLength; i++) { + byte b = da.readByte(); + if (b == '\n') { + if (byteOne) { // i.e. \n follows 1 + lastEntryStart = i+1; + // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) + break; + } + if (key == null || lastColon == -1 || i <= lastColon) { + throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev + } + value = new String(bos.toByteArray()).trim(); + bos.reset(); + _metadata.add(new MetadataEntry(key, value)); + key = value = null; + lastColon = -1; + lastEntryStart = i+1; + continue; + } + // byteOne has to be consumed up to this line, if not jet, consume it + if (byteOne) { + // insert 1 we've read on previous step into the byte builder + bos.write(1); + // fall-through to consume current byte + byteOne = false; + } + if (b == (int) ':') { + assert value == null; + key = new String(bos.toByteArray()); + bos.reset(); + lastColon = i; + } else if (b == 1) { + byteOne = true; + } else { + bos.write(b); + } + } + _metadata.trimToSize(); + metadata.add(revisionNumber, lastEntryStart, _metadata); + if (da.isEmpty() || !byteOne) { + throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); + } + // da is in prepared state (i.e. we consumed all bytes up to metadata end). + } + } }