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 }