/*
* Copyright (C) 2014 Civilian Framework.
*
* Licensed under the Civilian License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.civilian-framework.org/license.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.civilian.util;
import java.io.File;
/**
* Scanner helps to scan or parse strings.
*/
public class Scanner
{
/**
* Creates a new Scanner.
*/
public Scanner()
{
init((String)null);
}
/**
* Creates a new Scanner for the string.
*/
public Scanner(String input)
{
init(input);
}
/**
* Creates a new Scanner for the string.
* @param start start scanning at that index
*/
public Scanner(String input, int start)
{
init(input, start);
}
/**
* Creates a new Scanner for multiple lines.
*/
public Scanner(String... lines)
{
init(lines);
}
/**
* Sets the string which should be scanned.
*/
public void init(String input)
{
init(input, 0);
}
/**
* Sets the string which should be scanned.
* @param start start scanning at that index
*/
public void init(String input, int start)
{
initInput(input, start);
lines_ = new String[] { currentLine_ };
lineIndex_ = 0;
}
/**
* Sets the strings which should be scanned.
*/
public void init(String... lines)
{
if ((lines == null) || (lines.length == 0))
init("", 0);
else
{
initInput(lines[0], 0);
lines_ = lines;
lineIndex_ = 0;
}
}
private void initInput(String input, int start)
{
currentLine_ = input != null ? input : "";
length_ = currentLine_.length();
pos_ = Math.max(0, Math.min(start, length_));
needSkipWhitespace_ = autoSkipWhitespace_;
}
/**
* Stores the source of the scanner input.
*/
public void setSource(File source)
{
setSource(source != null ? source.getAbsolutePath() : null);
}
/**
* Stores the source of the scanner input.
*/
public void setSource(String source)
{
source_ = source;
}
/**
* Returns the source of the scanner data.
*/
public String getSource()
{
return source_;
}
/**
* Returns the lines on which the scanner is operating.
*/
public String[] getLines()
{
return lines_;
}
/**
* Returns the current scanner line index, starting at 0.
*/
public int getLineIndex()
{
return lineIndex_;
}
/**
* Returns the current scanner line index, starting at 0.
*/
public int getLineCount()
{
return lines_.length;
}
/**
* Returns the current scanner position within in the current line.
*/
public int getPos()
{
return pos_;
}
/**
* Returns the length of the current line.
*/
public int getLength()
{
return length_;
}
/**
* Returns the current character or -1 if there are no more characters.
*/
public String getLine()
{
return currentLine_;
}
/**
* Returns the current character or -1 if there are no more characters.
*/
public int current()
{
return hasMore() ? currentLine_.charAt(pos_) : -1;
}
/**
* Returns if the current character has the given character type.
* @param charType a character type defined in the Character class
* @see Character#getType(char)
*/
public boolean currentHasType(byte charType)
{
return Character.getType(current()) == charType;
}
/**
* Returns if the current character is a digit.
*/
public boolean currentIsDigit()
{
return currentHasType(Character.DECIMAL_DIGIT_NUMBER);
}
/**
* Sets if whitespace should automatically be skipped,
* before consume*, next* or expect*-operations are
* performed.
*/
public void autoSkipWhitespace(boolean flag)
{
autoSkipWhitespace_ = flag;
needSkipWhitespace_ = flag;
}
private void autoSkipWhitespace()
{
if (needSkipWhitespace_)
skipWhitespace();
}
/**
* Returns if there are more characters left in the
* current line.
*/
public boolean hasMore()
{
return pos_ < length_;
}
/**
* Returns if there are that much characters left in the current input string.
*/
public boolean hasMore(int length)
{
return pos_ + length <= length_;
}
private void advancePos(int n)
{
pos_ += n;
needSkipWhitespace_ = autoSkipWhitespace_;
}
/**
* Positions on the next character in the current line. (Does not
* skip any whitespace).
* @return true if positioned, false if there are no more characters left
*/
public boolean skip()
{
if (hasMore())
advancePos(1);
return hasMore();
}
/**
* Autoskips whitespace, and then tests (but does not consume)
* if the scanner string starts with the given string.
*/
public boolean match(String s)
{
autoSkipWhitespace();
int length = s.length();
return hasMore(length) && currentLine_.regionMatches(pos_, s, 0, length);
}
/**
* Positions the scanner on the next line
* @return false if the end of lines was reached
*/
public boolean nextLine()
{
if (lineIndex_ + 1 >= lines_.length)
return false;
else
{
initInput(lines_[++lineIndex_], 0);
return true;
}
}
/**
* Tests if the current line at the current positions starts with the given string.
* If true positions on the index after that prefix.
* Else does not move the scanner position.
* Autoskips whitespace before the test is made, if autoskip is turned on.
*/
public boolean next(String s)
{
return next(s, false);
}
/**
* Tests if the rest of the current line starts with the given string
* and is not immediately followed by a Java identifier char.
* If true positions on the index after that prefix.
* Else does not move the scanner position.
* Autoskips whitespace before the test is made, if autoskip is turned on.
*/
public boolean nextKeyword(String s)
{
return next(s, true);
}
private boolean next(String s, boolean testKeyword)
{
autoSkipWhitespace();
int length = s.length();
if (hasMore(length) && currentLine_.regionMatches(pos_, s, 0, length))
{
int last = pos_ + length;
if (last <= length_)
{
if (!testKeyword || (last == length_) || !Character.isJavaIdentifierPart(currentLine_.charAt(last)))
{
advancePos(length);
return true;
}
}
}
return false;
}
/**
* Consumes the first matching string.
* @return the matched string, or null if no string matches
*/
public String consumeAny(String... strings)
{
for (String s : strings)
{
if (next(s))
return s;
}
return null;
}
/**
* Returns the string upto the next whitespace boundary.
* If that string is empty or the end is reached null is returned.
*/
public String consumeToken()
{
return consumeToken("");
}
/**
* Returns the string upto the next whitespace boundary or upto
* one of the characters on the delimiter string is reached.
* If that string is empty or the end is reached null is returned.
*/
public String consumeToken(String delimiters)
{
String s = consumeUpto(delimiters, true, false, false);
if (s.length() > 0)
{
return s;
}
else
return null;
}
/**
* Moves the scanner position until one of the characters in the delimiter string
* is encountered.
* @param delimiters defines the delimiter characters
* @param whitespaceLimits if true encountered whitespace is also treated as delimiter
* @param needDelim if true and no delimiter is found, an exception is thrown.
* Else the whole rest of the line is consumed.
* @param skipDelim if true, position after the delimiter, else stop at the delimiter
* @return the consumed string
*/
public String consumeUpto(String delimiters, boolean whitespaceLimits, boolean needDelim, boolean skipDelim)
{
autoSkipWhitespace();
return consumeUptoNoSkip(delimiters, whitespaceLimits, needDelim, skipDelim);
}
private String consumeUptoNoSkip(String delimiters, boolean whitespaceLimits, boolean needDelim, boolean skipDelim)
{
int start = pos_;
if (delimiters == null)
delimiters = "";
while(hasMore())
{
char c = currentLine_.charAt(pos_++);
if ((delimiters.indexOf(c) >= 0))
return consumeUptoFound(start, skipDelim);
else if (whitespaceLimits && Character.isWhitespace(c))
return consumeUptoFound(start, false);
}
if (needDelim)
throw exception("expected one of '" + delimiters + "'");
needSkipWhitespace_ = autoSkipWhitespace_;
return currentLine_.substring(start);
}
private String consumeUptoFound(int start, boolean skipDelim)
{
needSkipWhitespace_ = autoSkipWhitespace_;
String s = currentLine_.substring(start, pos_ - 1);
if (!skipDelim)
pos_--;
return s;
}
/**
* Consumes all digit characters and return the string converted to an integer.
*/
public int consumeInt()
{
String s = consumeWhile(Character.DECIMAL_DIGIT_NUMBER);
if (s == null)
throw exception("expected a integer");
return Integer.parseInt(s);
}
/**
* Consumes all digit characters and an optional fraction part
* and returns the string converted to a double.
*/
public double consumeDouble()
{
autoSkipWhitespace();
int start = pos_;
increaseWhile(Character.DECIMAL_DIGIT_NUMBER, true);
if (next("."))
increaseWhile(Character.DECIMAL_DIGIT_NUMBER, true);
if (pos_ == start)
throw exception("expected a double");
return Double.parseDouble(currentLine_.substring(start, pos_));
}
/**
* Consumes hex encoded bytes.
*/
public byte[] consumeBytes()
{
String hex = consumeWhile("0123456789abcdefABCDEF");
byte[] b = new byte[hex.length() / 2];
int next = 0;
for (int i=0; i<b.length; i++)
{
b[i] = (byte)Integer.parseInt(hex.substring(next, next + 2), 16);
next += 2;
}
return b;
}
/**
* Move the scanner position while seeing characters of that type.
*/
public String consumeWhile(byte charType)
{
return consumeWhile(charType, true);
}
public String consumeWhile(byte charType, boolean equals)
{
autoSkipWhitespace();
int start = pos_;
increaseWhile(charType, equals);
return pos_ > start ? currentLine_.substring(start, pos_) : null;
}
private void increaseWhile(byte charType, boolean equals)
{
while (hasMore())
{
if ((Character.getType(currentLine_.charAt(pos_)) == charType) != equals)
break;
pos_++;
}
needSkipWhitespace_ = autoSkipWhitespace_;
}
/**
* Move the scanner position while seeing characters contained in
* the chars parameter.
*/
public String consumeWhile(String chars)
{
autoSkipWhitespace();
int start = pos_;
while (hasMore())
{
if (chars.indexOf(currentLine_.charAt(pos_)) == -1)
break;
pos_++;
}
needSkipWhitespace_ = autoSkipWhitespace_;
return pos_ > start ? currentLine_.substring(start, pos_) : null;
}
/**
* Scans a quoted string and returns the string without quotes.
*/
public String consumeQuotedString()
{
return consumeQuotedString(false);
}
/**
* Scans a quoted string and returns it.
* The current character is used as quote char.
* Does not autoskip whitespace.
* @param includeQuotes should the quotes be included?
*/
public String consumeQuotedString(boolean includeQuotes)
{
if (!hasMore())
return null;
char quote = currentLine_.charAt(pos_++);
String s = consumeUptoNoSkip(String.valueOf(quote), false, true, true);
needSkipWhitespace_ = autoSkipWhitespace_;
return includeQuotes ? quote + s + quote : s;
}
/**
* Returns the rest of the current line.
*/
public String consumeRest()
{
autoSkipWhitespace();
String s = getRest();
pos_ = length_;
needSkipWhitespace_ = autoSkipWhitespace_;
return s;
}
/**
* Scans a Java identifier and returns it.
*/
public String consumeIdentifier()
{
autoSkipWhitespace();
if (!Character.isJavaIdentifierStart(current()))
return null;
int start = pos_;
do
{
pos_++;
}
while(Character.isJavaIdentifierPart(current()));
needSkipWhitespace_ = autoSkipWhitespace_;
return currentLine_.substring(start, pos_);
}
/**
* Consumes a string. If this fails raise an exception.
*/
public void expect(String s)
{
if (!next(s))
throw exception("expected '" + s + "'");
}
/**
* Consume a character which must be contained in the given parameter
* string. If this fails raise an exception.
*/
public char expectOneOf(String s)
{
autoSkipWhitespace();
if (hasMore())
{
char c = currentLine_.charAt(pos_);
if (s.indexOf(c) >= 0)
{
pos_++;
needSkipWhitespace_ = autoSkipWhitespace_;
return c;
}
}
throw exception("expected one of '" + s + "'");
}
/**
* Skip all whitespace character. Progresses to the next
* line If the end of the line is reached
*/
public void skipWhitespace()
{
needSkipWhitespace_ = false;
while (true)
{
if (hasMore())
{
switch (currentLine_.charAt(pos_))
{
case '\n':
case ' ':
case '\r':
case '\t':
pos_++;
continue;
}
return;
}
if (!nextLine())
return;
}
}
/**
* Returns the rest of the current line.
*/
public String getRest()
{
return hasMore() ? currentLine_.substring(pos_) : "";
}
/**
* Raises an exception with context information about input and current position.
*/
public IllegalArgumentException exception(String message)
{
if (errorHandler_ != null)
errorHandler_.scanError(this, message);
StringBuilder s = new StringBuilder(message);
s.append(" (");
if (getLineCount() > 1)
{
s.append(getLineIndex() + 1);
s.append(':');
}
s.append(pos_ + 1);
s.append("): '");
s.append(currentLine_);
return new IllegalArgumentException(s.toString());
}
public void setErrorHandler(ErrorHandler errorHandler)
{
errorHandler_ = errorHandler;
}
/**
* A callback interface for scanners when they encounter an error.
*/
public static interface ErrorHandler
{
public void scanError(Scanner scanner, String message);
}
private int pos_;
private int length_;
private int lineIndex_;
private String currentLine_;
private String[] lines_;
private boolean autoSkipWhitespace_ = true;
private boolean needSkipWhitespace_;
private ErrorHandler errorHandler_;
private String source_;
}