com.redhat.ceylon.common.config.ConfigReader Maven / Gradle / Ivy
package com.redhat.ceylon.common.config;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PushbackReader;
import java.io.Reader;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.InvalidPropertiesFormatException;
/**
* A "push reader" for git-like Ceylon configuration files.
*
* @author Tako Schotanus ([email protected])
*/
public class ConfigReader {
private ConfigReaderListener listener;
private InputStream in;
private LineNumberReader counterdr;
private MemoPushbackReader reader;
private String section;
private enum Token { section, option, assign, comment, eol, error, eof }
/**
* Creates a new ConfigReader
* @param in The InputStream to read the actual file data from
* @param listener The listener to call when specific data items have been read and parsed
*/
public ConfigReader(InputStream in, ConfigReaderListener listener) {
this.in = in;
this.listener = listener;
}
/**
* Starts the actual processing of the input
* @throws IOException Either actual file-related IO exceptions or
* InvalidPropertiesFormatException when problems with the file format
* are detected
*/
public void process() throws IOException {
section = null;
try {
counterdr = new LineNumberReader(new BufferedReader(new InputStreamReader(in, Charset.forName("UTF-8"))));
reader = new MemoPushbackReader(counterdr);
listener.setup();
Token tok;
skipWhitespace(true);
flushWhitespace();
while ((tok = peekToken()) != Token.eof) {
switch (tok) {
case section:
handleSection();
break;
case option:
if (section != null) {
handleOption();
} else {
throw new InvalidPropertiesFormatException("Option without section in configuration file at line " + (counterdr.getLineNumber() + 1));
}
break;
case comment:
skipToNextLine();
listener.onComment(reader.getAndClearMemo());
break;
case eol:
skipToNextLine();
listener.onWhitespace(reader.getAndClearMemo());
break;
default:
throw new InvalidPropertiesFormatException("Unexpected token in configuration file at line " + (counterdr.getLineNumber() + 1));
}
skipWhitespace(true);
flushWhitespace();
}
listener.cleanup();
} finally {
if (reader != null) {
reader.close();
}
}
}
private void handleSection() throws IOException {
expect('[');
section = readName(true);
if (!section.matches("[\\p{L}\\p{Nd}]+(\\.[\\p{L}\\p{Nd}]+)*")) {
throw new InvalidPropertiesFormatException("Invalid section name in configuration file at line " + (counterdr.getLineNumber() + 1));
}
skipWhitespace(false);
if (reader.peek() == '\"') {
String subSection = readString();
expect('"');
section += "." + subSection;
skipWhitespace(false);
}
expect(']');
listener.onSection(section, reader.getAndClearMemo());
}
private void handleOption() throws IOException {
String option = readName(false);
String optName = section + "." + option;
skipWhitespace(false);
Token tok = peekToken();
if (tok == Token.assign) {
expect('=');
handleOptionValue(optName);
} else if (tok == Token.error) {
throw new InvalidPropertiesFormatException("Unexpected token in configuration file at line " + (counterdr.getLineNumber() + 1));
} else {
listener.onOption(optName, "true", reader.getAndClearMemo());
}
}
private String readName(boolean forSection) throws IOException {
StringBuilder str = new StringBuilder();
int c;
while ((c = reader.read()) != -1) {
if ((!forSection && isOptionNameChar(c)) || (forSection && isSectionNameChar(c))) {
str.append((char)c);
} else {
reader.unread(c);
break;
}
}
return str.toString();
}
private String readString() throws IOException {
StringBuilder str = new StringBuilder();
gobble('\"');
int c;
while ((c = reader.read()) != -1) {
if (c == '"') {
reader.unread(c);
break;
} else if (c == '\\') {
int c2 = reader.read();
if (c2 == '\\') {
// Do nothing
} else if (c2 == '\"') {
c = c2;
} else {
throw new InvalidPropertiesFormatException("Illegal escape character in configuration file at line " + (counterdr.getLineNumber() + 1));
}
}
str.append((char)c);
}
return str.toString();
}
private void handleOptionValue(String optName) throws IOException {
StringBuilder str = new StringBuilder();
skipWhitespace(false);
boolean hasQuote = gobble('\"');
int c;
while ((c = reader.read()) != -1) {
if (c == '"') {
reader.unread(c);
break;
} else if (isNewLineChar(c)) {
reader.unread(c);
break;
} else if (isCommentChar(c) && !hasQuote) {
reader.unread(c);
break;
} else if (c == '\\') {
int c2 = reader.read();
if (c2 == '\\') {
// Do nothing
} else if (c2 == '\"') {
c = c2;
} else if (c2 == 't') {
c = '\t';
} else if (c2 == 'n') {
c = '\n';
} else if (isNewLineChar(c2)) {
skipNewLine(c2);
c = '\n';
} else {
throw new InvalidPropertiesFormatException("Illegal escape character in configuration file at line " + (counterdr.getLineNumber() + 1));
}
}
str.append((char)c);
}
String res = str.toString();
if (hasQuote) {
expect('\"');
listener.onOption(optName, res, reader.getAndClearMemo());
} else {
String memo = reader.getAndClearMemo();
// Is there still some whitespace?
String ws = rightTrimmings(res);
if (!ws.isEmpty()) {
listener.onOption(optName, res.trim(), memo.trim());
listener.onWhitespace(ws);
} else {
listener.onOption(optName, res.trim(), memo);
}
}
}
private String rightTrimmings(String txt) {
int st = txt.length();
char[] val = txt.toCharArray();
while ((st > 0) && (val[st - 1] <= ' ')) {
st--;
}
return (st > 0) ? txt.substring(st) : txt;
}
private void expect(int expected) throws IOException {
int c;
if ((c = reader.read()) != expected) {
throw new InvalidPropertiesFormatException("Unexpected token in configuration file at line " + (counterdr.getLineNumber() + 1) + ", expected '" + Character.valueOf((char)expected) + "' but got '" + Character.valueOf((char)c) + "'");
}
}
private void skipWhitespace(boolean multiline) throws IOException {
int c;
while ((c = reader.read()) != -1) {
if (!Character.isWhitespace(c) || (!multiline && isNewLineChar(c))) {
reader.unread(c);
break;
}
}
}
private void skipToNextLine() throws IOException {
int c;
while ((c = reader.read()) != -1) {
if (isNewLineChar(c)) {
skipNewLine(c);
break;
}
}
}
private Token peekToken() throws IOException {
int c = reader.peek();
if (isCommentChar(c)) {
return Token.comment;
} else if (c == '[') {
return Token.section;
} else if (c == '=') {
return Token.assign;
} else if (isNewLineChar(c)) {
return Token.eol;
} else if (isOptionNameChar(c)) {
return Token.option;
} else if (c == -1) {
return Token.eof;
} else {
return Token.error;
}
}
private boolean gobble(int chr) throws IOException {
int c = reader.read();
if (c != chr) {
reader.unread(c);
return false;
}
return true;
}
private void skipNewLine(int c) throws IOException {
if (c == '\r') {
c = reader.read();
if (c != '\n') {
reader.unread(c);
}
}
}
private void flushWhitespace() throws IOException {
String ws = reader.getAndClearMemo();
while (!ws.isEmpty()) {
String txt;
int p = ws.indexOf('\n');
if (p >= 0) {
txt = ws.substring(0, p + 1);
ws = ws.substring(p + 1);
} else {
txt = ws;
ws = "";
}
listener.onWhitespace(txt);
}
}
private boolean isOptionNameChar(int c) {
return Character.isLetterOrDigit(c) || c == '-';
}
private boolean isSectionNameChar(int c) {
return isOptionNameChar(c) || c == '.';
}
private boolean isCommentChar(int c) {
return c == ';' || c == '#';
}
private boolean isNewLineChar(int c) {
return c == '\n' || c == '\r';
}
}
/*
* Special "pushback" reader that combines the functionality of
* pushback (the ability to "peek" at the next character in a
* stream or pushing back the last character read) with a "memo"
* feature that records the last string of characters read since
* the last call to getAndClearMemo().
* This last feature is used to be able to retrieve the "actual
* characters read" that activated a certain event in the config
* reader listener.
* For example, when reading the string (without []):
* [ "true" ]
* the actual value is "true" (without the quotes) but the string
* of characters that was read was actually much longer, most of
* which was actually discarded. The memo feature exists to be
* able to easily retrieve that actual string of characters.
*/
class MemoPushbackReader extends PushbackReader {
private StringBuilder memo;
public MemoPushbackReader(Reader in) {
super(in);
memo = new StringBuilder(1024);
}
@Override
public int read() throws IOException {
int c = super.read();
if (c != -1) {
memo.append((char)c);
}
return c;
}
@Override
public void unread(int c) throws IOException {
super.unread(c);
memo.setLength(memo.length() - 1);
}
public int peek() throws IOException {
int c = super.read();
if (c != -1) {
super.unread(c);
}
return c;
}
@Override
public void reset() throws IOException {
super.reset();
memo.setLength(0);
}
public String getAndClearMemo() {
String result = memo.toString();
memo.setLength(0);
return result;
}
// All the following methods we don't really need so they aren't implemented
// to prevent anybody from accidentally using them they throw an error
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public long skip(long n) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void unread(char[] cbuf, int off, int len) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void unread(char[] cbuf) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public int read(char[] cbuf) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public int read(CharBuffer target) throws IOException {
throw new UnsupportedOperationException();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy