tikhomirov@17: /* tikhomirov@74: * Copyright (c) 2010-2011 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@2: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@2: tikhomirov@148: import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; tikhomirov@148: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@74: tikhomirov@157: import java.io.ByteArrayOutputStream; tikhomirov@148: import java.io.IOException; tikhomirov@115: import java.nio.ByteBuffer; tikhomirov@78: import java.util.ArrayList; tikhomirov@78: import java.util.Collection; tikhomirov@79: import java.util.TreeMap; tikhomirov@78: tikhomirov@148: import org.tmatesoft.hg.core.HgDataStreamException; tikhomirov@157: import org.tmatesoft.hg.core.HgException; tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@157: import org.tmatesoft.hg.internal.DataAccess; tikhomirov@121: import org.tmatesoft.hg.internal.FilterByteChannel; tikhomirov@77: import org.tmatesoft.hg.internal.RevlogStream; tikhomirov@115: import org.tmatesoft.hg.util.ByteChannel; tikhomirov@148: import org.tmatesoft.hg.util.CancelledException; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@74: tikhomirov@5: tikhomirov@17: tikhomirov@2: /** tikhomirov@2: * ? name:HgFileNode? tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@2: */ tikhomirov@2: public class HgDataFile extends Revlog { tikhomirov@2: tikhomirov@3: // absolute from repo root? tikhomirov@3: // slashes, unix-style? tikhomirov@3: // repo location agnostic, just to give info to user, not to access real storage tikhomirov@74: private final Path path; tikhomirov@134: private Metadata metadata; // get initialized on first access to file content. tikhomirov@2: tikhomirov@115: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) { tikhomirov@21: super(hgRepo, content); tikhomirov@115: path = filePath; tikhomirov@3: } tikhomirov@115: tikhomirov@115: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) { tikhomirov@115: super(hgRepo); tikhomirov@115: path = filePath; tikhomirov@115: } tikhomirov@115: tikhomirov@115: // exists is not the best name possible. now it means no file with such name was ever known to the repo. tikhomirov@115: // it might be confused with files existed before but lately removed. tikhomirov@3: public boolean exists() { tikhomirov@3: return content != null; // XXX need better impl tikhomirov@2: } tikhomirov@2: tikhomirov@77: // human-readable (i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i") tikhomirov@74: public Path getPath() { tikhomirov@157: return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? tikhomirov@2: } tikhomirov@2: tikhomirov@22: public int length(Nodeid nodeid) { tikhomirov@88: return content.dataLength(getLocalRevision(nodeid)); tikhomirov@22: } tikhomirov@22: tikhomirov@157: public void workingCopy(ByteChannel sink) throws IOException, CancelledException { tikhomirov@157: throw HgRepository.notImplemented(); tikhomirov@2: } tikhomirov@115: tikhomirov@157: // public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { tikhomirov@157: // byte[] content = content(revision); tikhomirov@157: // final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); tikhomirov@157: // final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); tikhomirov@157: // ByteBuffer buf = ByteBuffer.allocate(512); tikhomirov@157: // int left = content.length; tikhomirov@157: // progressSupport.start(left); tikhomirov@157: // int offset = 0; tikhomirov@157: // cancelSupport.checkCancelled(); tikhomirov@157: // ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; tikhomirov@157: // do { tikhomirov@157: // buf.put(content, offset, Math.min(left, buf.remaining())); tikhomirov@157: // buf.flip(); tikhomirov@157: // cancelSupport.checkCancelled(); tikhomirov@157: // // XXX I may not rely on returned number of bytes but track change in buf position instead. tikhomirov@157: // int consumed = _sink.write(buf); tikhomirov@157: // buf.compact(); tikhomirov@157: // offset += consumed; tikhomirov@157: // left -= consumed; tikhomirov@157: // progressSupport.worked(consumed); tikhomirov@157: // } while (left > 0); tikhomirov@157: // progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. tikhomirov@157: // } tikhomirov@157: tikhomirov@157: /*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/ tikhomirov@157: public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { tikhomirov@157: content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); tikhomirov@115: } tikhomirov@22: tikhomirov@78: // for data files need to check heading of the file content for possible metadata tikhomirov@78: // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- tikhomirov@157: public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { tikhomirov@78: if (revision == TIP) { tikhomirov@135: revision = getLastRevision(); tikhomirov@78: } tikhomirov@157: if (revision == WORKING_COPY) { tikhomirov@157: workingCopy(sink); tikhomirov@157: return; tikhomirov@157: } tikhomirov@157: if (wrongLocalRevision(revision) || revision == BAD_REVISION) { tikhomirov@148: throw new IllegalArgumentException(String.valueOf(revision)); tikhomirov@148: } tikhomirov@157: if (sink == null) { tikhomirov@157: throw new IllegalArgumentException(); tikhomirov@157: } tikhomirov@134: if (metadata == null) { tikhomirov@134: metadata = new Metadata(); tikhomirov@134: } tikhomirov@157: ContentPipe insp; tikhomirov@134: if (metadata.none(revision)) { tikhomirov@157: insp = new ContentPipe(sink, 0); tikhomirov@157: } else if (metadata.known(revision)) { tikhomirov@157: insp = new ContentPipe(sink, metadata.dataOffset(revision)); tikhomirov@157: } else { tikhomirov@157: // do not know if there's metadata tikhomirov@157: insp = new MetadataContentPipe(sink, metadata); tikhomirov@78: } tikhomirov@157: insp.checkCancelled(); tikhomirov@157: super.content.iterate(revision, revision, true, insp); tikhomirov@157: try { tikhomirov@157: insp.checkFailed(); tikhomirov@157: } catch (HgDataStreamException ex) { tikhomirov@157: throw ex; tikhomirov@157: } catch (HgException ex) { tikhomirov@157: // shall not happen, unless we changed ContentPipe or its subclass tikhomirov@157: throw new HgDataStreamException(ex.getClass().getName(), ex); tikhomirov@78: } tikhomirov@78: } tikhomirov@157: tikhomirov@129: public void history(HgChangelog.Inspector inspector) { tikhomirov@135: history(0, getLastRevision(), inspector); tikhomirov@48: } tikhomirov@48: tikhomirov@129: public void history(int start, int end, HgChangelog.Inspector inspector) { tikhomirov@3: if (!exists()) { tikhomirov@3: throw new IllegalStateException("Can't get history of invalid repository file node"); tikhomirov@3: } tikhomirov@135: final int last = getLastRevision(); tikhomirov@77: if (start < 0 || start > last) { tikhomirov@77: throw new IllegalArgumentException(); tikhomirov@77: } tikhomirov@77: if (end == TIP) { tikhomirov@77: end = last; tikhomirov@77: } else if (end < start || end > last) { tikhomirov@77: throw new IllegalArgumentException(); tikhomirov@77: } tikhomirov@48: final int[] commitRevisions = new int[end - start + 1]; tikhomirov@77: RevlogStream.Inspector insp = new RevlogStream.Inspector() { tikhomirov@3: int count = 0; tikhomirov@3: tikhomirov@51: public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { tikhomirov@3: commitRevisions[count++] = linkRevision; tikhomirov@3: } tikhomirov@3: }; tikhomirov@48: content.iterate(start, end, false, insp); tikhomirov@3: getRepo().getChangelog().range(inspector, commitRevisions); tikhomirov@3: } tikhomirov@88: tikhomirov@88: // for a given local revision of the file, find out local revision in the changelog tikhomirov@88: public int getChangesetLocalRevision(int revision) { tikhomirov@88: return content.linkRevision(revision); tikhomirov@88: } tikhomirov@88: tikhomirov@88: public Nodeid getChangesetRevision(Nodeid nid) { tikhomirov@88: int changelogRevision = getChangesetLocalRevision(getLocalRevision(nid)); tikhomirov@88: return getRepo().getChangelog().getRevision(changelogRevision); tikhomirov@88: } tikhomirov@78: tikhomirov@157: public boolean isCopy() throws HgDataStreamException { tikhomirov@134: if (metadata == null || !metadata.checked(0)) { tikhomirov@134: // content() always initializes metadata. tikhomirov@157: // FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. tikhomirov@157: try { tikhomirov@157: content(0, new ByteChannel() { // No-op channel tikhomirov@157: public int write(ByteBuffer buffer) throws IOException { tikhomirov@157: // pretend we consumed whole buffer tikhomirov@157: int rv = buffer.remaining(); tikhomirov@157: buffer.position(buffer.limit()); tikhomirov@157: return rv; tikhomirov@157: } tikhomirov@157: }); tikhomirov@157: } catch (Exception ex) { tikhomirov@157: throw new HgDataStreamException("Can't initialize metadata", ex); tikhomirov@17: } tikhomirov@78: } tikhomirov@134: if (!metadata.known(0)) { tikhomirov@78: return false; tikhomirov@78: } tikhomirov@78: return metadata.find(0, "copy") != null; tikhomirov@78: } tikhomirov@78: tikhomirov@157: public Path getCopySourceName() throws HgDataStreamException { tikhomirov@78: if (isCopy()) { tikhomirov@78: return Path.create(metadata.find(0, "copy")); tikhomirov@78: } tikhomirov@78: throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?) tikhomirov@78: } tikhomirov@78: tikhomirov@157: public Nodeid getCopySourceRevision() throws HgDataStreamException { tikhomirov@78: if (isCopy()) { tikhomirov@78: return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid tikhomirov@78: } tikhomirov@78: throw new UnsupportedOperationException(); tikhomirov@78: } tikhomirov@88: tikhomirov@88: @Override tikhomirov@88: public String toString() { tikhomirov@88: StringBuilder sb = new StringBuilder(getClass().getSimpleName()); tikhomirov@88: sb.append('('); tikhomirov@88: sb.append(getPath()); tikhomirov@88: sb.append(')'); tikhomirov@88: return sb.toString(); tikhomirov@88: } tikhomirov@78: tikhomirov@88: private static final class MetadataEntry { tikhomirov@78: private final String entry; tikhomirov@78: private final int valueStart; tikhomirov@78: /*package-local*/MetadataEntry(String key, String value) { tikhomirov@78: entry = key + value; tikhomirov@78: valueStart = key.length(); tikhomirov@78: } tikhomirov@78: /*package-local*/boolean matchKey(String key) { tikhomirov@78: return key.length() == valueStart && entry.startsWith(key); tikhomirov@78: } tikhomirov@134: // uncomment once/if needed tikhomirov@134: // public String key() { tikhomirov@134: // return entry.substring(0, valueStart); tikhomirov@134: // } tikhomirov@78: public String value() { tikhomirov@78: return entry.substring(valueStart); tikhomirov@78: } tikhomirov@78: } tikhomirov@78: tikhomirov@78: private static class Metadata { tikhomirov@78: // XXX sparse array needed tikhomirov@79: private final TreeMap offsets = new TreeMap(); tikhomirov@79: private final TreeMap entries = new TreeMap(); tikhomirov@134: tikhomirov@134: private final Integer NONE = new Integer(-1); // do not duplicate -1 integers at least within single file (don't want statics) tikhomirov@134: tikhomirov@134: // true when there's metadata for given revision tikhomirov@78: boolean known(int revision) { tikhomirov@134: Integer i = offsets.get(revision); tikhomirov@134: return i != null && NONE != i; tikhomirov@134: } tikhomirov@134: tikhomirov@134: // true when revision has been checked for metadata presence. tikhomirov@134: public boolean checked(int revision) { tikhomirov@78: return offsets.containsKey(revision); tikhomirov@78: } tikhomirov@134: tikhomirov@134: // true when revision has been checked and found not having any metadata tikhomirov@134: boolean none(int revision) { tikhomirov@134: Integer i = offsets.get(revision); tikhomirov@134: return i == NONE; tikhomirov@134: } tikhomirov@134: tikhomirov@134: // mark revision as having no metadata. tikhomirov@134: void recordNone(int revision) { tikhomirov@134: Integer i = offsets.get(revision); tikhomirov@134: if (i == NONE) { tikhomirov@134: return; // already there tikhomirov@134: } tikhomirov@134: if (i != null) { tikhomirov@134: throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); tikhomirov@134: } tikhomirov@134: offsets.put(revision, NONE); tikhomirov@134: } tikhomirov@134: tikhomirov@78: // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) tikhomirov@78: int dataOffset(int revision) { tikhomirov@78: return offsets.get(revision); tikhomirov@78: } tikhomirov@78: void add(int revision, int dataOffset, Collection e) { tikhomirov@134: assert !offsets.containsKey(revision); tikhomirov@78: offsets.put(revision, dataOffset); tikhomirov@78: entries.put(revision, e.toArray(new MetadataEntry[e.size()])); tikhomirov@78: } tikhomirov@78: String find(int revision, String key) { tikhomirov@78: for (MetadataEntry me : entries.get(revision)) { tikhomirov@78: if (me.matchKey(key)) { tikhomirov@78: return me.value(); tikhomirov@78: } tikhomirov@78: } tikhomirov@78: return null; tikhomirov@78: } tikhomirov@78: } tikhomirov@157: tikhomirov@157: private static class MetadataContentPipe extends ContentPipe { tikhomirov@157: tikhomirov@157: private final Metadata metadata; tikhomirov@157: tikhomirov@157: public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { tikhomirov@157: super(sink, 0); tikhomirov@157: metadata = _metadata; tikhomirov@157: } tikhomirov@157: tikhomirov@157: @Override tikhomirov@157: protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { tikhomirov@158: final int daLength = da.length(); tikhomirov@157: if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { tikhomirov@157: metadata.recordNone(revisionNumber); tikhomirov@157: da.reset(); tikhomirov@157: return; tikhomirov@157: } tikhomirov@157: int lastEntryStart = 2; tikhomirov@157: int lastColon = -1; tikhomirov@157: ArrayList _metadata = new ArrayList(); tikhomirov@157: // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, tikhomirov@157: // which can't be used here because we can't convert bytes to chars as we read them tikhomirov@157: // (there might be multi-byte encoding), and we need to collect all bytes before converting to string tikhomirov@157: ByteArrayOutputStream bos = new ByteArrayOutputStream(); tikhomirov@157: String key = null, value = null; tikhomirov@157: boolean byteOne = false; tikhomirov@157: for (int i = 2; i < daLength; i++) { tikhomirov@157: byte b = da.readByte(); tikhomirov@157: if (b == '\n') { tikhomirov@157: if (byteOne) { // i.e. \n follows 1 tikhomirov@157: lastEntryStart = i+1; tikhomirov@157: // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) tikhomirov@157: break; tikhomirov@157: } tikhomirov@157: if (key == null || lastColon == -1 || i <= lastColon) { tikhomirov@157: throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev tikhomirov@157: } tikhomirov@157: value = new String(bos.toByteArray()).trim(); tikhomirov@157: bos.reset(); tikhomirov@157: _metadata.add(new MetadataEntry(key, value)); tikhomirov@157: key = value = null; tikhomirov@157: lastColon = -1; tikhomirov@157: lastEntryStart = i+1; tikhomirov@157: continue; tikhomirov@157: } tikhomirov@157: // byteOne has to be consumed up to this line, if not jet, consume it tikhomirov@157: if (byteOne) { tikhomirov@157: // insert 1 we've read on previous step into the byte builder tikhomirov@157: bos.write(1); tikhomirov@157: // fall-through to consume current byte tikhomirov@157: byteOne = false; tikhomirov@157: } tikhomirov@157: if (b == (int) ':') { tikhomirov@157: assert value == null; tikhomirov@157: key = new String(bos.toByteArray()); tikhomirov@157: bos.reset(); tikhomirov@157: lastColon = i; tikhomirov@157: } else if (b == 1) { tikhomirov@157: byteOne = true; tikhomirov@157: } else { tikhomirov@157: bos.write(b); tikhomirov@157: } tikhomirov@157: } tikhomirov@157: _metadata.trimToSize(); tikhomirov@157: metadata.add(revisionNumber, lastEntryStart, _metadata); tikhomirov@157: if (da.isEmpty() || !byteOne) { tikhomirov@157: throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); tikhomirov@157: } tikhomirov@157: // da is in prepared state (i.e. we consumed all bytes up to metadata end). tikhomirov@17: } tikhomirov@17: } tikhomirov@2: }