1 /*
2 * #%L
3 * JavaHg
4 * %%
5 * Copyright (C) 2011 aragost Trifork ag
6 * %%
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 * #L%
25 */
26 package com.aragost.javahg.internals;
27
28 import java.io.BufferedInputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.nio.charset.CharsetDecoder;
33
34 import com.aragost.javahg.DateTime;
35
36 /**
37 * An InputStream that has some methods that make it convenient for
38 * JavaHg to read the stdout from the command server.
39 */
40 public class HgInputStream extends BufferedInputStream {
41
42 private CharsetDecoder textDecoder;
43
44 /**
45 * @param in
46 * the byte stream.
47 * @param textDecoder
48 * the decoder used when Strings are extracted from the
49 * byte stream.
50 */
51 public HgInputStream(InputStream in, CharsetDecoder textDecoder) {
52 super(in);
53 this.textDecoder = textDecoder;
54 }
55
56 /**
57 * Return the next byte from the stream without forwarding the
58 * position.
59 *
60 * @return the byte read or -1 if EOF is reached.
61 * @throws IOException
62 */
63 public int peek() throws IOException {
64 mark(1);
65 int result = read();
66 reset();
67 return result;
68 }
69
70 /**
71 * @return true if no more bytes can be read from the stream
72 * @throws IOException
73 */
74 public boolean isEof() throws IOException {
75 return peek() == -1;
76 }
77
78 /**
79 * Get the next {@code length} bytes from the stream.
80 *
81 * @param length
82 * the number of bytes to read.
83 * @return the bytes read or {@code null} if EOF is reached before
84 * all the bytes have been read.
85 * @throws IOException
86 */
87 public byte[] next(int length) throws IOException {
88 byte[] bytes = new byte[length];
89 int remaining = length;
90 while (remaining > 0) {
91 int n = read(bytes, length - remaining, remaining);
92 if (n == -1) {
93 return null;
94 }
95 remaining = remaining - n;
96 }
97 return bytes;
98 }
99
100 /**
101 * Get the next {@code length} bytes from the stream, and return
102 * it as a String
103 *
104 * @param length
105 * @return the decoded String
106 * @throws IOException
107 */
108 public String nextAsText(int length) throws IOException {
109 byte[] bytes = next(length);
110 if (bytes == null) {
111 return null;
112 } else {
113 return Utils.decodeBytes(bytes, this.textDecoder);
114 }
115 }
116
117 /**
118 * Look for a fixed set of bytes in the stream. The current
119 * position is advanced by the length of the bytes to look for if
120 * they are found, otherwise it is left unchanged.
121 *
122 * @param bytes
123 * the bytes to look for
124 * @return true if the bytes were found at the current position.
125 * @throws IOException
126 */
127 public boolean match(byte[] bytes) throws IOException {
128 mark(bytes.length);
129 for (int i = 0; i < bytes.length; i++) {
130 int n = read();
131 if (n == -1 || (byte) n != bytes[i]) {
132 reset();
133 return false;
134 }
135 }
136 return true;
137 }
138
139 /**
140 * Look for a fixed byte in the stream. The current position is
141 * advanced by 1 if the byte is found, otherwise it is left
142 * unchanged.
143 *
144 * @param b
145 * the byte to look for.
146 * @return true if the byte was found at the current position.
147 * @throws IOException
148 */
149 public boolean match(int b) throws IOException {
150 mark(1);
151 int n = read();
152 if (n == b) {
153 return true;
154 } else {
155 reset();
156 return false;
157 }
158 }
159
160 /**
161 * Verifies that the next bytes in the stream matches the
162 * specified bytes.
163 *
164 * @param bytes
165 * @throws IOException
166 * @throws UnexpectedCommandOutputException
167 * if the stream doesn't match the specified bytes
168 */
169 public void mustMatch(byte[] bytes) throws IOException, UnexpectedCommandOutputException {
170 for (int i = 0; i < bytes.length; i++) {
171 mustMatch(bytes[i]);
172 }
173 }
174
175 /**
176 * Verifies that the next byte in the stream matches the specified
177 * byte.
178 *
179 * @param b
180 * the next byte
181 * @throws IOException
182 * @throws UnexpectedCommandOutputException
183 * if the stream doesn't match the specified bytes
184 */
185 public void mustMatch(int b) throws IOException, UnexpectedCommandOutputException {
186 int n = read();
187 if ((byte) n != b) {
188 throw new UnexpectedCommandOutputException("Got " + n + ", but expected " + b);
189 }
190 }
191
192 /**
193 * Read from the stream until a fixed set of bytes are found. The
194 * current position is left after the stop bytes.
195 *
196 * @param stop
197 * the bytes to look for.
198 * @return the bytes read while looking for the stop bytes. This
199 * does not include the stop bytes themselves.
200 * @throws IOException
201 */
202 public byte[] upTo(byte[] stop) throws IOException {
203 if (stop.length == 0) {
204 return new byte[0];
205 }
206 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(80);
207 int stopIndex = 0;
208
209 while (true) {
210 int b = read();
211 if (b == -1) {
212 return null;
213 // byteStream.write(stop, 0, stopIndex);
214 // break;
215 }
216 if (stop[stopIndex] == (byte) b) {
217 if (stopIndex == 0) {
218 mark(stop.length);
219 }
220 stopIndex++;
221 if (stopIndex == stop.length) {
222 break;
223 }
224 } else {
225 if (stopIndex > 0) {
226 byteStream.write(stop, 0, 1);
227 reset();
228 stopIndex = 0;
229 } else {
230 byteStream.write(b);
231 }
232 }
233 }
234 return byteStream.toByteArray();
235 }
236
237 /**
238 * Read from the stream until a fixed byte is found. The current
239 * position is left after the stop byte.
240 *
241 * @param stop
242 * the byte to look for.
243 * @return the bytes read while looking for the stop byte. This
244 * does not include the stop byte.
245 * @throws IOException
246 */
247 public byte[] upTo(int stop) throws IOException {
248 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(40);
249 while (true) {
250 int n = read();
251 if (n == -1) {
252 return null;
253 }
254 if (n == stop) {
255 break;
256 }
257 byteStream.write(n);
258 }
259 return byteStream.toByteArray();
260 }
261
262 /**
263 * Search for the specified bytes in the stream. If found then the
264 * position in the stream is just after the bytes. If not found,
265 * the stream is positioned at EOF.
266 *
267 * @param bytes
268 * @return true if the bytes were found, false otherwise
269 * @throws IOException
270 */
271 public boolean find(byte[] bytes) throws IOException {
272 int index = 0;
273 int length = bytes.length;
274 if (length == 0) {
275 throw new IllegalArgumentException("Can't search for nothing");
276 }
277 while (true) {
278 int b = read();
279 if (b == -1) {
280 return false;
281 }
282 if (bytes[index] == (byte) b) {
283 if (index == 0) {
284 mark(length);
285 }
286 index++;
287 if (index == length) {
288 return true;
289 }
290 } else if (index > 0) {
291 reset();
292 index = 0;
293 }
294 }
295 }
296
297 /**
298 * Read from stream until the specified byte is read.
299 *
300 * @param b
301 * @return true if the byte was found, otherwise false
302 * @throws IOException
303 */
304 public boolean find(int b) throws IOException {
305 while (true) {
306 int n = read();
307 if (n == -1) {
308 return false;
309 }
310 if (n == b) {
311 return true;
312 }
313 }
314
315 }
316
317 /**
318 * Read a non-negative integer from the stream until a fixed byte
319 * is found. The current position is left after the stop byte.
320 *
321 * @param stop
322 * the byte to look for.
323 * @return the integer read.
324 * @throws IOException
325 */
326 public int decimalIntUpTo(int stop) throws IOException {
327 int result = 0;
328 while (true) {
329 int n = read();
330 if (n == -1 || n == stop) {
331 return result;
332 }
333 int digit = n - '0';
334 if (digit < 0 || digit >= 10) {
335 throw new IOException("A non-digit found: " + (char) n);
336 }
337 result = result * 10 + digit;
338 }
339 }
340
341 /**
342 * Read a non-negative integer from the stream.
343 *
344 * All characters that are a valid digit is read.
345 *
346 * @return null if the next character is a non-digit, otherwise
347 * return the integer value of the digit characters read
348 * from stream
349 * @throws IOException
350 */
351 public Integer readDecimal() throws IOException {
352 boolean somethingRead = false;
353 int result = 0;
354 while (true) {
355 mark(1);
356 int n = read();
357 if (n == -1) {
358 break;
359 }
360 int digit = n - '0';
361 if (digit >= 0 && digit < 10) {
362 somethingRead = true;
363 result = 10 * result + digit;
364 } else {
365 reset();
366 break;
367 }
368 }
369 return somethingRead ? Integer.valueOf(result) : null;
370 }
371
372 /**
373 * Read a revision number from the stream until a fixed byte is
374 * found. A revision number is an integer greater than or equal to
375 * -1. The current position is left after the stop byte.
376 * <p>
377 * Initial spaces in the stream is skipped until a '-' or a digit is found.
378 *
379 * @param stop
380 * the byte to look for.
381 * @return the integer read.
382 * @throws IOException
383 */
384 public int revisionUpTo(int stop) throws IOException {
385 while (peek() == ' ') {
386 read();
387 }
388 if (peek() == '-') {
389 read();
390 mustMatch('1');
391 mustMatch(stop);
392 return -1;
393 } else {
394 return decimalIntUpTo(stop);
395 }
396 }
397
398 /**
399 * Read a Mercurial date from the stream, stopping when a fixed
400 * byte is met. A Mercurial date is produced with the "hgdate"
401 * template filter and consist of two integers, the first is the
402 * number of seconds since 1970 and the second is the time zone
403 * offset.
404 *
405 * @param stopByte
406 * the stop byte
407 * @return a parsed date
408 * @throws IOException
409 */
410 public DateTime dateTimeUpTo(int stopByte) throws IOException {
411 long millis = 1000L * decimalIntUpTo(' ');
412 boolean negative = match('-');
413 int timezoneOffset = 1000 * decimalIntUpTo(stopByte);
414 if (negative) {
415 timezoneOffset = -timezoneOffset;
416 }
417 return new DateTime(millis, timezoneOffset);
418 }
419
420 /**
421 * Read from the stream until {@code end} is found, return the
422 * read portion as a String. The current position is left after
423 * the {@code end} marker.
424 *
425 * @param end
426 * the stop marker.
427 * @return the decoded bytes
428 * @throws IOException
429 */
430 public String textUpTo(byte[] end) throws IOException {
431 byte[] bytes = upTo(end);
432 if (bytes == null) {
433 return null;
434 } else {
435 return Utils.decodeBytes(bytes, this.textDecoder);
436 }
437 }
438
439 /**
440 * Read from the stream until the byte {@code b} is found, return
441 * the read portion as a String. The current position is left
442 * after the {@code b} marker.
443 *
444 * @param b
445 * the stop marker.
446 * @return the decoded bytes
447 * @throws IOException
448 */
449 public String textUpTo(int b) throws IOException {
450 byte[] bytes = upTo(b);
451 if (bytes == null) {
452 return null;
453 } else {
454 return Utils.decodeBytes(bytes, this.textDecoder);
455 }
456
457 }
458
459 /**
460 * Read until EOF and discard the bytes read
461 *
462 * @throws IOException
463 */
464 public void consumeAll() throws IOException {
465 Utils.consumeAll(this);
466 }
467
468 @Override
469 public String toString() {
470 // Overridden for debugability
471 StringBuilder buffer = new StringBuilder();
472
473 buffer.append(new String(buf, 0, this.pos));
474 buffer.append(">@<");
475 buffer.append(new String(buf, this.pos, this.count));
476
477 return buffer.toString();
478 }
479 }