/*
* #%L
* JavaHg
* %%
* Copyright (C) 2011 aragost Trifork ag
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.aragost.javahg.internals;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharsetDecoder;
import com.aragost.javahg.DateTime;
/**
* An InputStream that has some methods that make it convenient for
* JavaHg to read the stdout from the command server.
*/
public class HgInputStream extends BufferedInputStream {
private CharsetDecoder textDecoder;
/**
* @param in
* the byte stream.
* @param textDecoder
* the decoder used when Strings are extracted from the
* byte stream.
*/
public HgInputStream(InputStream in, CharsetDecoder textDecoder) {
super(in);
this.textDecoder = textDecoder;
}
/**
* Return the next byte from the stream without forwarding the
* position.
*
* @return the byte read or -1 if EOF is reached.
* @throws IOException
*/
public int peek() throws IOException {
mark(1);
int result = read();
reset();
return result;
}
/**
* @return true if no more bytes can be read from the stream
* @throws IOException
*/
public boolean isEof() throws IOException {
return peek() == -1;
}
/**
* Get the next {@code length} bytes from the stream.
*
* @param length
* the number of bytes to read.
* @return the bytes read or {@code null} if EOF is reached before
* all the bytes have been read.
* @throws IOException
*/
public byte[] next(int length) throws IOException {
byte[] bytes = new byte[length];
int remaining = length;
while (remaining > 0) {
int n = read(bytes, length - remaining, remaining);
if (n == -1) {
return null;
}
remaining = remaining - n;
}
return bytes;
}
/**
* Get the next {@code length} bytes from the stream, and return
* it as a String
*
* @param length
* @return the decoded String
* @throws IOException
*/
public String nextAsText(int length) throws IOException {
byte[] bytes = next(length);
if (bytes == null) {
return null;
} else {
return Utils.decodeBytes(bytes, this.textDecoder);
}
}
/**
* Look for a fixed set of bytes in the stream. The current
* position is advanced by the length of the bytes to look for if
* they are found, otherwise it is left unchanged.
*
* @param bytes
* the bytes to look for
* @return true if the bytes were found at the current position.
* @throws IOException
*/
public boolean match(byte[] bytes) throws IOException {
mark(bytes.length);
for (int i = 0; i < bytes.length; i++) {
int n = read();
if (n == -1 || (byte) n != bytes[i]) {
reset();
return false;
}
}
return true;
}
/**
* Look for a fixed byte in the stream. The current position is
* advanced by 1 if the byte is found, otherwise it is left
* unchanged.
*
* @param b
* the byte to look for.
* @return true if the byte was found at the current position.
* @throws IOException
*/
public boolean match(int b) throws IOException {
mark(1);
int n = read();
if (n == b) {
return true;
} else {
reset();
return false;
}
}
/**
* Verifies that the next bytes in the stream matches the
* specified bytes.
*
* @param bytes
* @throws IOException
* @throws UnexpectedCommandOutputException
* if the stream doesn't match the specified bytes
*/
public void mustMatch(byte[] bytes) throws IOException, UnexpectedCommandOutputException {
for (int i = 0; i < bytes.length; i++) {
mustMatch(bytes[i]);
}
}
/**
* Verifies that the next byte in the stream matches the specified
* byte.
*
* @param b
* the next byte
* @throws IOException
* @throws UnexpectedCommandOutputException
* if the stream doesn't match the specified bytes
*/
public void mustMatch(int b) throws IOException, UnexpectedCommandOutputException {
int n = read();
if ((byte) n != b) {
throw new UnexpectedCommandOutputException("Got " + n + ", but expected " + b);
}
}
/**
* Read from the stream until a fixed set of bytes are found. The
* current position is left after the stop bytes.
*
* @param stop
* the bytes to look for.
* @return the bytes read while looking for the stop bytes. This
* does not include the stop bytes themselves.
* @throws IOException
*/
public byte[] upTo(byte[] stop) throws IOException {
if (stop.length == 0) {
return new byte[0];
}
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(80);
int stopIndex = 0;
while (true) {
int b = read();
if (b == -1) {
return null;
// byteStream.write(stop, 0, stopIndex);
// break;
}
if (stop[stopIndex] == (byte) b) {
if (stopIndex == 0) {
mark(stop.length);
}
stopIndex++;
if (stopIndex == stop.length) {
break;
}
} else {
if (stopIndex > 0) {
byteStream.write(stop, 0, 1);
reset();
stopIndex = 0;
} else {
byteStream.write(b);
}
}
}
return byteStream.toByteArray();
}
/**
* Read from the stream until a fixed byte is found. The current
* position is left after the stop byte.
*
* @param stop
* the byte to look for.
* @return the bytes read while looking for the stop byte. This
* does not include the stop byte.
* @throws IOException
*/
public byte[] upTo(int stop) throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(40);
while (true) {
int n = read();
if (n == -1) {
return null;
}
if (n == stop) {
break;
}
byteStream.write(n);
}
return byteStream.toByteArray();
}
/**
* Search for the specified bytes in the stream. If found then the
* position in the stream is just after the bytes. If not found,
* the stream is positioned at EOF.
*
* @param bytes
* @return true if the bytes were found, false otherwise
* @throws IOException
*/
public boolean find(byte[] bytes) throws IOException {
int index = 0;
int length = bytes.length;
if (length == 0) {
throw new IllegalArgumentException("Can't search for nothing");
}
while (true) {
int b = read();
if (b == -1) {
return false;
}
if (bytes[index] == (byte) b) {
if (index == 0) {
mark(length);
}
index++;
if (index == length) {
return true;
}
} else if (index > 0) {
reset();
index = 0;
}
}
}
/**
* Read from stream until the specified byte is read.
*
* @param b
* @return true if the byte was found, otherwise false
* @throws IOException
*/
public boolean find(int b) throws IOException {
while (true) {
int n = read();
if (n == -1) {
return false;
}
if (n == b) {
return true;
}
}
}
/**
* Read a non-negative integer from the stream until a fixed byte
* is found. The current position is left after the stop byte.
*
* @param stop
* the byte to look for.
* @return the integer read.
* @throws IOException
*/
public int decimalIntUpTo(int stop) throws IOException {
int result = 0;
while (true) {
int n = read();
if (n == -1 || n == stop) {
return result;
}
int digit = n - '0';
if (digit < 0 || digit >= 10) {
throw new IOException("A non-digit found: " + (char) n);
}
result = result * 10 + digit;
}
}
/**
* Read a non-negative integer from the stream.
*
* All characters that are a valid digit is read.
*
* @return null if the next character is a non-digit, otherwise
* return the integer value of the digit characters read
* from stream
* @throws IOException
*/
public Integer readDecimal() throws IOException {
boolean somethingRead = false;
int result = 0;
while (true) {
mark(1);
int n = read();
if (n == -1) {
break;
}
int digit = n - '0';
if (digit >= 0 && digit < 10) {
somethingRead = true;
result = 10 * result + digit;
} else {
reset();
break;
}
}
return somethingRead ? Integer.valueOf(result) : null;
}
/**
* Read a revision number from the stream until a fixed byte is
* found. A revision number is an integer greater than or equal to
* -1. The current position is left after the stop byte.
* <p>
* Initial spaces in the stream is skipped until a '-' or a digit is found.
*
* @param stop
* the byte to look for.
* @return the integer read.
* @throws IOException
*/
public int revisionUpTo(int stop) throws IOException {
while (peek() == ' ') {
read();
}
if (peek() == '-') {
read();
mustMatch('1');
mustMatch(stop);
return -1;
} else {
return decimalIntUpTo(stop);
}
}
/**
* Read a Mercurial date from the stream, stopping when a fixed
* byte is met. A Mercurial date is produced with the "hgdate"
* template filter and consist of two integers, the first is the
* number of seconds since 1970 and the second is the time zone
* offset.
*
* @param stopByte
* the stop byte
* @return a parsed date
* @throws IOException
*/
public DateTime dateTimeUpTo(int stopByte) throws IOException {
long millis = 1000L * decimalIntUpTo(' ');
boolean negative = match('-');
int timezoneOffset = 1000 * decimalIntUpTo(stopByte);
if (negative) {
timezoneOffset = -timezoneOffset;
}
return new DateTime(millis, timezoneOffset);
}
/**
* Read from the stream until {@code end} is found, return the
* read portion as a String. The current position is left after
* the {@code end} marker.
*
* @param end
* the stop marker.
* @return the decoded bytes
* @throws IOException
*/
public String textUpTo(byte[] end) throws IOException {
byte[] bytes = upTo(end);
if (bytes == null) {
return null;
} else {
return Utils.decodeBytes(bytes, this.textDecoder);
}
}
/**
* Read from the stream until the byte {@code b} is found, return
* the read portion as a String. The current position is left
* after the {@code b} marker.
*
* @param b
* the stop marker.
* @return the decoded bytes
* @throws IOException
*/
public String textUpTo(int b) throws IOException {
byte[] bytes = upTo(b);
if (bytes == null) {
return null;
} else {
return Utils.decodeBytes(bytes, this.textDecoder);
}
}
/**
* Read until EOF and discard the bytes read
*
* @throws IOException
*/
public void consumeAll() throws IOException {
Utils.consumeAll(this);
}
@Override
public String toString() {
// Overridden for debugability
StringBuilder buffer = new StringBuilder();
buffer.append(new String(buf, 0, this.pos));
buffer.append(">@<");
buffer.append(new String(buf, this.pos, this.count));
return buffer.toString();
}
@Override
public void close() throws IOException {
consumeAll();
super.close();
}
}