package org.kisst.props4j;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Parser {
private static final Logger logger = LoggerFactory.getLogger(SimpleProps.class);
private final File file;
private final BufferedReader inp;
private char lastchar;
private boolean eof=false;
private boolean useLastChar=false;
private int line=1;
private int pos=0;
public Parser(Reader inp, File f) {
this.file=f;
if (inp instanceof BufferedReader)
this.inp=(BufferedReader) inp;
else
this.inp=new BufferedReader(inp);
}
public Parser(InputStream inpstream) { this(new InputStreamReader(inpstream), null); }
//private File getFile() { return file; }
private File getPath(String path) {
if (file==null)
return new File(path);
else if (file.isDirectory())
return new File(file,path);
else
return new File(file.getParent(), path);
}
private char getLastChar() { return lastchar; }
private boolean eof() {return eof; }
private void unread() { useLastChar=true; }
private char read() {
if (useLastChar) {
useLastChar=false;
return lastchar;
}
int ch;
try {
do {
ch = inp.read();
}
while (ch=='\r'); // ignore all carriage returns
} catch (IOException e) { throw new ParseException(e); }
if (ch=='\n') {
line++; pos=0;
}
else
pos++;
if (ch<0)
eof=true;
else
lastchar=(char)ch;
return lastchar;
}
@SuppressWarnings("unused")
private String readIdentifier() {
skipWhitespaceAndComments();
StringBuilder result=new StringBuilder();
while (! eof()){
char ch=read();
if (Character.isLetterOrDigit(ch) || ch=='_')
result.append(ch);
else {
unread();
break;
}
}
return result.toString();
}
private String readIdentifierPath() {
skipWhitespaceAndComments();
StringBuilder result=new StringBuilder();
while (! eof()){
char ch=read();
if (Character.isLetterOrDigit(ch) || ch=='.' || ch=='_')
result.append(ch);
else {
unread();
break;
}
}
return result.toString();
}
private String readDoubleQuotedString() { return readUntil("\"").trim(); }
//private String readSingleQuotedString() { return readUntil("\'").trim(); }
private String readUnquotedString() { return readUntil("\n").trim(); }
private String readUntil(String endchars) {
StringBuilder result=new StringBuilder();
while (! eof()){
char ch=read();
if (eof())
break;
if (ch=='\\') {
ch=read();
if (eof())
break;
if (ch!='\n')
result.append(ch);
}
else {
if (endchars.indexOf(ch)>=0)
break;
result.append(ch);
}
}
if (eof()) {
if (result.length()==0)
return null;
}
return result.toString();
}
private void skipWhitespaceAndComments() {
while (! eof()){
char ch=read();
if (ch=='#') {
skipLine();
continue;
}
if (ch!=' ' && ch!='\t' && ch!='\n' && ch!='\r') {
unread();
return;
}
}
}
private void skipLine() {
while (! eof()){
char ch=read();
if (ch=='\n')
break;
}
}
public class ParseException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ParseException(String message) {
super("Parse exception: file "+file+", line:"+line+" pos: "+pos+": "+message);
}
public ParseException(Exception e) {
super("Parse exception: file "+file+", line:"+line+" pos: "+pos+": "+e.getMessage(), e);
}
}
private Object readObject() { return readObject(null, null); }
private Object readObject(SimpleProps parent, String name) {
skipWhitespaceAndComments();
while (! eof()){
char ch=read();
if (eof())
return null;
if (ch == '{' ) {
return readMap(parent, name);
}
else if (ch == '[' )
return readList();
else if (ch == '(' )
return readParamList();
else if (ch == ' ' || ch == '\t' || ch == '\n')
continue;
else if (ch=='"')
return replaceVars(readDoubleQuotedString(), parent, name);
else if (Character.isLetterOrDigit(ch) || ch=='/' || ch=='.' || ch==':' || ch=='$') {
String result=ch+readUnquotedString();
if (parent==null || name==null)
return result;
return replaceVars(result,parent, name);
}
else if (ch=='@')
return readSpecialObject();
}
return null;
}
private String replaceVars(String str, Props props, String logname) {
if (logname==null)
throw new RuntimeException(str+"\t"+props);
if (str.startsWith("dynamic:"))
return str;
int pos=str.indexOf("${");
if (pos<0)
return str;
StringBuilder result = new StringBuilder();
int pos0=0;
while (pos>=0) {
int pos2=str.indexOf("}",pos);
if (pos2<0)
throw new ParseException("${ without ending }");
result.append(str.subSequence(pos0, pos));
String var=str.substring(pos+2,pos2);
boolean toLowerCase=false;
boolean toUpperCase=false;
if (var.endsWith("?lower_case")) {
var=var.substring(0, var.length()-11);
toLowerCase = true;
}
if (var.endsWith("?upper_case")) {
var=var.substring(0, var.length()-11);
toUpperCase = true;
}
String value= searchValue(var, props);
if (value==null) {
logger.error("In ***"+logname+" could not substitute variable ${"+var+"} in expression "+str);
value="??"+var+"??";
}
if (toLowerCase)
value=value.toLowerCase();
else if (toUpperCase)
value=value.toUpperCase();
result.append(value);
pos0=pos2+1;
pos=str.indexOf("${",pos0);
}
result.append(str.substring(pos0));
logger.info("Variable substitution for var {} from {} to "+result.toString(), logname, str);
return result.toString();
}
private String searchValue(String var, Props props) {
if (var.trim().startsWith("ENV:")) {
var=var.trim().substring(4);
String result = System.getProperty(var);
if (result!=null)
return result;
return System.getenv(var);
}
Props p=props;
String value=null;
while (p!=null) {
value=p.getString(var, null); // TODO: should also work if not a String
if (value!=null)
break;
p=p.getParent();
}
return value;
}
private Object readSpecialObject() {
String type=readUntil("(;").trim();
if (type.equals("file")) {
String filename=readUntil(")").trim();
return getPath(filename);
}
else if (type.equals("null"))
return null;
else
throw new ParseException("Unknown special object type @"+type);
}
private Object readList() {
// TODO Auto-generated method stub
return null;
}
private Object readParamList() {
// A paramlist is like a list, but may also contain keyword parameters
// TODO: currently it is just like an object with parenthesis
Object result=readObject();
skipWhitespaceAndComments();
if (getLastChar()!=')')
throw new ParseException("parameter list should end with )");
return result;
}
private SimpleProps readMap(SimpleProps parent, String name) {
SimpleProps map=new SimpleProps(parent, name);
fillMap(map);
return map;
}
public void fillMap(SimpleProps map) {
while (! eof()) {
skipWhitespaceAndComments();
char ch=read();
if (ch=='}')
break; // map has ended
else if (ch=='@'){
String cmd=readIdentifierPath();
if (cmd.equals("include"))
include(map,this);
continue;
}
else if (ch==';')
continue; // ignore
else if (Character.isLetter(ch) || ch=='_') {
unread();
String key=readIdentifierPath();
skipWhitespaceAndComments();
if (getLastChar() == '=' || getLastChar() ==':' ) {
SimpleProps keyparent=map.getParentForKeyWithCreate(key);
map.put(key, readObject(keyparent, key));
}
else if (getLastChar() == '+') {
char ch2 = read();
if (ch2 != '=')
throw new ParseException("+ should only be used in +=");
throw new ParseException("+= not yet supported");
}
else
throw new ParseException("field assignment "+key+" in map "+map.getFullName()+" should have =, : or +=, not "+ch);
}
else if (eof())
break;
else
throw new ParseException("when parsing map "+map.getFullName()+" unexpected character "+getLastChar());
}
}
private void include(SimpleProps map, Parser inp) {
boolean recurse=false;
String postfix=null;
Object o=readObject();
File f=null;
if (o instanceof File)
f=(File) o;
else if (o instanceof String) {
String name=(String) o;
if (name.indexOf("${")>0)
name=replaceVars(name,map,"@include");
int pos=name.indexOf("**/*");
if (pos>0) {
postfix=name.substring(pos+4);
recurse=true;
name=name.substring(0,pos);
}
pos=name.indexOf('*');
if (pos>0) {
postfix=name.substring(pos+1);
name=name.substring(0,pos);
}
f=inp.getPath(name);
}
else
throw inp.new ParseException("unknown type of object to include "+o);
if (f.isFile())
map.load(f);
else if (f.isDirectory())
includeDir(map, f, postfix, recurse);
}
private void includeDir(SimpleProps map, File dir, String postfix, boolean recurse) {
File[] files = dir.listFiles(); // TODO: filter
for (File f2: files) {
if (f2.isDirectory() && recurse)
includeDir(map, f2, postfix, recurse);
else {
if (postfix!=null && ! f2.getName().endsWith(postfix))
continue;
if (f2.isFile())
map.load(f2);
}
}
}
}