view src/org/tmatesoft/hg/internal/Metadata.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents 7efabe0cddcf
children
line wrap: on
line source
/*
 * Copyright (c) 2013 TMate Software Ltd
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@hg4j.com
 */
package org.tmatesoft.hg.internal;

import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
import static org.tmatesoft.hg.util.LogFacility.Severity.Error;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

import org.tmatesoft.hg.core.SessionContext;
import org.tmatesoft.hg.repo.HgInvalidControlFileException;
import org.tmatesoft.hg.repo.HgInvalidStateException;
import org.tmatesoft.hg.util.LogFacility;

/**
 * Container for metadata recorded as part of file revisions
 * 
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public final class Metadata {
	private static class Record {
		public final int offset;
		public final MetadataEntry[] entries;
		
		public Record(int off, MetadataEntry[] entr) {
			offset = off;
			entries = entr;
		}
	}
	// XXX sparse array needed
	private final IntMap<Metadata.Record> entries = new IntMap<Metadata.Record>(5);
	
	private final Metadata.Record NONE = new Record(-1, null); // don't want statics

	private final LogFacility log;
	
	private int lastRevRead = NO_REVISION;

	public Metadata(SessionContext.Source sessionCtx) {
		log = sessionCtx.getSessionContext().getLog();
	}
	
	/**
	 * @return <code>true</code> when there's metadata for given revision
	 */
	public boolean known(int revision) {
		Metadata.Record i = entries.get(revision);
		return i != null && NONE != i;
	}

	/**
	 * @return <code>true</code> when revision has been checked for metadata presence.
	 */
	public boolean checked(int revision) {
		return entries.containsKey(revision);
	}

	// true when revision has been checked and found not having any metadata
	public boolean none(int revision) {
		Metadata.Record i = entries.get(revision);
		return i == NONE;
	}
	
	/**
	 * Get the greatest revision index visited so far.
	 * Note, doesn't imply all revisions up to this has been visited.
	 */
	public int lastRevisionRead() {
		return lastRevRead;
	}

	// mark revision as having no metadata.
	void recordNone(int revision) {
		Metadata.Record i = entries.get(revision);
		if (i == NONE) {
			return; // already there
		} 
		if (i != null) {
			throw new HgInvalidStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i));
		}
		entries.put(revision, NONE);
	}

	// since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before)
	public int dataOffset(int revision) {
		return entries.get(revision).offset;
	}
	void add(int revision, int dataOffset, Collection<MetadataEntry> e) {
		assert !entries.containsKey(revision);
		entries.put(revision, new Record(dataOffset, e.toArray(new MetadataEntry[e.size()])));
	}
	
	/**
	 * @return <code>true</code> if metadata has been found
	 */
	public boolean tryRead(int revisionNumber, DataAccess data) throws IOException, HgInvalidControlFileException {
		final int daLength = data.length();
		if (lastRevRead == NO_REVISION || revisionNumber > lastRevRead) {
			lastRevRead = revisionNumber;
		}
		if (daLength < 4 || data.readByte() != 1 || data.readByte() != 10) {
			recordNone(revisionNumber);
			return false;
		} else {
			ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>();
			int offset = parseMetadata(data, daLength, _metadata);
			add(revisionNumber, offset, _metadata);
			return true;
		}
	}

	public String find(int revision, String key) {
		for (MetadataEntry me : entries.get(revision).entries) {
			if (me.matchKey(key)) {
				return me.value();
			}
		}
		return null;
	}

	private int parseMetadata(DataAccess data, final int daLength, ArrayList<MetadataEntry> _metadata) throws IOException, HgInvalidControlFileException {
		int lastEntryStart = 2;
		int lastColon = -1;
		// 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;
		boolean metadataIsComplete = false;
		for (int i = 2; i < daLength; i++) {
			byte b = data.readByte();
			if (b == '\n') {
				if (byteOne) { // i.e. \n follows 1
					lastEntryStart = i+1;
					metadataIsComplete = true;
					// XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n)
					// if yes, need to set metadataIsComplete to true in that case as well
					break;
				}
				if (key == null || lastColon == -1 || i <= lastColon) {
					log.dump(getClass(), Error, "Missing key in file revision metadata at index %d", i);
				}
				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 yet, consume it
			if (byteOne) {
				// insert 1 we've read on previous step into the byte builder
				bos.write(1);
				byteOne = false;
				// fall-through to consume current byte
			}
			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);
			}
		}
		// data.isEmpty is not reliable, renamed files of size==0 keep only metadata
		if (!metadataIsComplete) {
			// XXX perhaps, worth a testcase (empty file, renamed, read or ask ifCopy
			throw new HgInvalidControlFileException("Metadata is not closed properly", null, null);
		}
		return lastEntryStart;
	}

	/**
	 * There may be several entries of metadata per single revision, this class captures single entry
	 */
	private static class MetadataEntry {
		private final String entry;
		private final int valueStart;

		// key may be null
		/* package-local */MetadataEntry(String key, String value) {
			if (key == null) {
				entry = value;
				valueStart = -1; // not 0 to tell between key == null and key == ""
			} else {
				entry = key + value;
				valueStart = key.length();
			}
		}

		/* package-local */boolean matchKey(String key) {
			return key == null ? valueStart == -1 : key.length() == valueStart && entry.startsWith(key);
		}

//			uncomment once/if needed
//			public String key() {
//				return entry.substring(0, valueStart);
//			}

		public String value() {
			return valueStart == -1 ? entry : entry.substring(valueStart);
		}
	}
}