Mercurial > hg4j
comparison src/org/tmatesoft/hg/internal/Metadata.java @ 602:e3717fc7d26f
Refactor metadata parsing in HgDataFile, moved to standalone class
| author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
|---|---|
| date | Mon, 06 May 2013 17:11:29 +0200 |
| parents | |
| children | 7efabe0cddcf |
comparison
equal
deleted
inserted
replaced
| 601:8143c1f77d45 | 602:e3717fc7d26f |
|---|---|
| 1 /* | |
| 2 * Copyright (c) 2013 TMate Software Ltd | |
| 3 * | |
| 4 * This program is free software; you can redistribute it and/or modify | |
| 5 * it under the terms of the GNU General Public License as published by | |
| 6 * the Free Software Foundation; version 2 of the License. | |
| 7 * | |
| 8 * This program is distributed in the hope that it will be useful, | |
| 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 11 * GNU General Public License for more details. | |
| 12 * | |
| 13 * For information on how to redistribute this software under | |
| 14 * the terms of a license other than GNU General Public License | |
| 15 * contact TMate Software at support@hg4j.com | |
| 16 */ | |
| 17 package org.tmatesoft.hg.internal; | |
| 18 | |
| 19 import static org.tmatesoft.hg.util.LogFacility.Severity.Error; | |
| 20 | |
| 21 import java.io.ByteArrayOutputStream; | |
| 22 import java.io.IOException; | |
| 23 import java.util.ArrayList; | |
| 24 import java.util.Collection; | |
| 25 | |
| 26 import org.tmatesoft.hg.core.SessionContext; | |
| 27 import org.tmatesoft.hg.repo.HgInvalidControlFileException; | |
| 28 import org.tmatesoft.hg.repo.HgInvalidStateException; | |
| 29 import org.tmatesoft.hg.util.LogFacility; | |
| 30 | |
| 31 /** | |
| 32 * Container for metadata recorded as part of file revisions | |
| 33 * | |
| 34 * @author Artem Tikhomirov | |
| 35 * @author TMate Software Ltd. | |
| 36 */ | |
| 37 public final class Metadata { | |
| 38 private static class Record { | |
| 39 public final int offset; | |
| 40 public final MetadataEntry[] entries; | |
| 41 | |
| 42 public Record(int off, MetadataEntry[] entr) { | |
| 43 offset = off; | |
| 44 entries = entr; | |
| 45 } | |
| 46 } | |
| 47 // XXX sparse array needed | |
| 48 private final IntMap<Metadata.Record> entries = new IntMap<Metadata.Record>(5); | |
| 49 | |
| 50 private final Metadata.Record NONE = new Record(-1, null); // don't want statics | |
| 51 | |
| 52 private final LogFacility log; | |
| 53 | |
| 54 public Metadata(SessionContext.Source sessionCtx) { | |
| 55 log = sessionCtx.getSessionContext().getLog(); | |
| 56 } | |
| 57 | |
| 58 // true when there's metadata for given revision | |
| 59 public boolean known(int revision) { | |
| 60 Metadata.Record i = entries.get(revision); | |
| 61 return i != null && NONE != i; | |
| 62 } | |
| 63 | |
| 64 // true when revision has been checked for metadata presence. | |
| 65 public boolean checked(int revision) { | |
| 66 return entries.containsKey(revision); | |
| 67 } | |
| 68 | |
| 69 // true when revision has been checked and found not having any metadata | |
| 70 public boolean none(int revision) { | |
| 71 Metadata.Record i = entries.get(revision); | |
| 72 return i == NONE; | |
| 73 } | |
| 74 | |
| 75 // mark revision as having no metadata. | |
| 76 void recordNone(int revision) { | |
| 77 Metadata.Record i = entries.get(revision); | |
| 78 if (i == NONE) { | |
| 79 return; // already there | |
| 80 } | |
| 81 if (i != null) { | |
| 82 throw new HgInvalidStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); | |
| 83 } | |
| 84 entries.put(revision, NONE); | |
| 85 } | |
| 86 | |
| 87 // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) | |
| 88 public int dataOffset(int revision) { | |
| 89 return entries.get(revision).offset; | |
| 90 } | |
| 91 void add(int revision, int dataOffset, Collection<MetadataEntry> e) { | |
| 92 assert !entries.containsKey(revision); | |
| 93 entries.put(revision, new Record(dataOffset, e.toArray(new MetadataEntry[e.size()]))); | |
| 94 } | |
| 95 | |
| 96 /** | |
| 97 * @return <code>true</code> if metadata has been found | |
| 98 */ | |
| 99 public boolean tryRead(int revisionNumber, DataAccess data) throws IOException, HgInvalidControlFileException { | |
| 100 final int daLength = data.length(); | |
| 101 if (daLength < 4 || data.readByte() != 1 || data.readByte() != 10) { | |
| 102 recordNone(revisionNumber); | |
| 103 return false; | |
| 104 } else { | |
| 105 ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>(); | |
| 106 int offset = parseMetadata(data, daLength, _metadata); | |
| 107 add(revisionNumber, offset, _metadata); | |
| 108 return true; | |
| 109 } | |
| 110 } | |
| 111 | |
| 112 public String find(int revision, String key) { | |
| 113 for (MetadataEntry me : entries.get(revision).entries) { | |
| 114 if (me.matchKey(key)) { | |
| 115 return me.value(); | |
| 116 } | |
| 117 } | |
| 118 return null; | |
| 119 } | |
| 120 | |
| 121 private int parseMetadata(DataAccess data, final int daLength, ArrayList<MetadataEntry> _metadata) throws IOException, HgInvalidControlFileException { | |
| 122 int lastEntryStart = 2; | |
| 123 int lastColon = -1; | |
| 124 // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, | |
| 125 // which can't be used here because we can't convert bytes to chars as we read them | |
| 126 // (there might be multi-byte encoding), and we need to collect all bytes before converting to string | |
| 127 ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
| 128 String key = null, value = null; | |
| 129 boolean byteOne = false; | |
| 130 boolean metadataIsComplete = false; | |
| 131 for (int i = 2; i < daLength; i++) { | |
| 132 byte b = data.readByte(); | |
| 133 if (b == '\n') { | |
| 134 if (byteOne) { // i.e. \n follows 1 | |
| 135 lastEntryStart = i+1; | |
| 136 metadataIsComplete = true; | |
| 137 // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) | |
| 138 // if yes, need to set metadataIsComplete to true in that case as well | |
| 139 break; | |
| 140 } | |
| 141 if (key == null || lastColon == -1 || i <= lastColon) { | |
| 142 log.dump(getClass(), Error, "Missing key in file revision metadata at index %d", i); | |
| 143 } | |
| 144 value = new String(bos.toByteArray()).trim(); | |
| 145 bos.reset(); | |
| 146 _metadata.add(new MetadataEntry(key, value)); | |
| 147 key = value = null; | |
| 148 lastColon = -1; | |
| 149 lastEntryStart = i+1; | |
| 150 continue; | |
| 151 } | |
| 152 // byteOne has to be consumed up to this line, if not yet, consume it | |
| 153 if (byteOne) { | |
| 154 // insert 1 we've read on previous step into the byte builder | |
| 155 bos.write(1); | |
| 156 byteOne = false; | |
| 157 // fall-through to consume current byte | |
| 158 } | |
| 159 if (b == (int) ':') { | |
| 160 assert value == null; | |
| 161 key = new String(bos.toByteArray()); | |
| 162 bos.reset(); | |
| 163 lastColon = i; | |
| 164 } else if (b == 1) { | |
| 165 byteOne = true; | |
| 166 } else { | |
| 167 bos.write(b); | |
| 168 } | |
| 169 } | |
| 170 // data.isEmpty is not reliable, renamed files of size==0 keep only metadata | |
| 171 if (!metadataIsComplete) { | |
| 172 // XXX perhaps, worth a testcase (empty file, renamed, read or ask ifCopy | |
| 173 throw new HgInvalidControlFileException("Metadata is not closed properly", null, null); | |
| 174 } | |
| 175 return lastEntryStart; | |
| 176 } | |
| 177 | |
| 178 /** | |
| 179 * There may be several entries of metadata per single revision, this class captures single entry | |
| 180 */ | |
| 181 private static class MetadataEntry { | |
| 182 private final String entry; | |
| 183 private final int valueStart; | |
| 184 | |
| 185 // key may be null | |
| 186 /* package-local */MetadataEntry(String key, String value) { | |
| 187 if (key == null) { | |
| 188 entry = value; | |
| 189 valueStart = -1; // not 0 to tell between key == null and key == "" | |
| 190 } else { | |
| 191 entry = key + value; | |
| 192 valueStart = key.length(); | |
| 193 } | |
| 194 } | |
| 195 | |
| 196 /* package-local */boolean matchKey(String key) { | |
| 197 return key == null ? valueStart == -1 : key.length() == valueStart && entry.startsWith(key); | |
| 198 } | |
| 199 | |
| 200 // uncomment once/if needed | |
| 201 // public String key() { | |
| 202 // return entry.substring(0, valueStart); | |
| 203 // } | |
| 204 | |
| 205 public String value() { | |
| 206 return valueStart == -1 ? entry : entry.substring(valueStart); | |
| 207 } | |
| 208 } | |
| 209 } |
