/*
* <copyright>
* Copyright 2010 BBN Technologies
* </copyright>
*/
package com.bbn.openmap.layer.vpf;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.bbn.openmap.io.FormatException;
/**
* This parser class takes a string representing a logic statement and parses it
* into objects that can be used for evaluating attributes of features. It's
* based on expressions specified in the GeoSym Handbook MIL-HDBK-857A.
* <P>
* It can parse the expression given per the specification, such as
* 42|1|bfc|1|0|1, or the strings specified in the symbol tables in the
* MIL-DTL-89045A, like bfc=81 AND sta<>0and<>1and<>2and<>3and<>5and<>6and<>11.
* <P>
*
* @author dietrick
*/
public class GeoSymAttExpression {
private static Logger logger = Logger.getLogger("com.bbn.openmap.layer.vpf.GeoSymAttExpr");
public final static int NO_OP = 0;
public final static int EQUALS_OP = 1;
public final static int NOT_EQUALS_OP = 2;
public final static int LESS_THAN_OP = 3;
public final static int GREATER_THAN_OP = 4;
public final static int LT_EQUALS_OP = 5;
public final static int GT_EQUALS_OP = 6;
public final static int NONE_CONN = 0;
public final static int or_CONN = 1; // same attribute can be this or that
public final static int AND_CONN = 2; // different attributes must all be
// this and this
public final static int and_CONN = 3; // same attribute must be this and that
public final static int OR_CONN = 4; // one attribute can be this or a
// different attribute can be that
protected VPFAutoFeatureGraphicWarehouse warehouse;
protected Expression exp;
/**
* The NOOP goes first to preserve the index value of each operator, as
* specifed in the GeoSym spec.
*/
protected static String[] ops = new String[] {
"",
"=",
"<>",
"<",
">",
"<=",
">="
};
/**
* Create the expression object given a text representation of it.
*
* @param source
* @param warehouse used to resolve the ECDIS variables.
*/
public GeoSymAttExpression(String source, VPFAutoFeatureGraphicWarehouse warehouse) {
// Warehouse must be set first.
this.warehouse = warehouse;
exp = findExpression(source);
if (logger.isLoggable(Level.FINER)) {
logger.finer("Parsing: " + source);
logger.finer(this.toString());
}
}
protected Connector findOp(String source) {
int ANDIndex = source.lastIndexOf("AND");
int ORIndex = source.lastIndexOf("OR");
if (ANDIndex == ORIndex) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("connector not found in " + source);
}
// both -1;
return null;
}
if (ANDIndex > ORIndex) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("found AND in " + source);
}
return new Connector(AND_CONN, ANDIndex);
} else {
if (logger.isLoggable(Level.FINER)) {
logger.finer("found OR in " + source);
}
return new Connector(OR_CONN, ORIndex);
}
}
public String toString() {
if (exp != null) {
return exp.toString();
} else {
return "No Expression Defined";
}
}
protected Connector findMiniOp(String source) {
int ANDIndex = source.lastIndexOf("and");
int ORIndex = source.lastIndexOf("or");
if (ANDIndex == ORIndex) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("connector not found in " + source);
}
// both -1;
return null;
}
if (ANDIndex > ORIndex) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("found and in " + source);
}
return new Connector(and_CONN, ANDIndex);
} else {
if (logger.isLoggable(Level.FINER)) {
logger.finer("found or in " + source);
}
return new Connector(or_CONN, ORIndex);
}
}
protected Expression findMathOp(String source) {
int opIndex = 1, locIndex = -1;
Expression exp = null;
// Need to make sure that the finding one op doesn't obscure another,
// i.e. finding = but missing <=.
while (opIndex < 7) {
locIndex = source.indexOf(ops[opIndex]);
if (locIndex >= 0) {
if (opIndex == 1 || opIndex == 3 || opIndex == 4) {
if (source.contains("<=") || source.contains(">=")) {
opIndex++;
continue;
} else {
break;
}
} else {
break;
}
}
opIndex++;
}
if (locIndex != -1) {
// Check out right side. If string, then create CompareExpression. If
// number, ValueExpression
String rightSide = source.substring(locIndex + ops[opIndex].length());
String leftSide = null;
if (locIndex > 0) {
leftSide = source.substring(0, locIndex);
}
if (logger.isLoggable(Level.FINER)) {
logger.finer("got left side: " + leftSide + " op: " + ops[opIndex] + " and right side: " + rightSide);
}
/**
* So here, We need to make a determination of whether the the left
* side is a column name from the data, as specified in the FCI, or if
* it's a ECDIS check. If the right side is a numerical value, we're
* just looking to test attribute data against hard numbers. We're
* going to push this decision into the ValueExpression, let it figure
* out what it should do.
*/
try {
Double val = Double.parseDouble(rightSide);
/**
* We need to check the length of this String to see if it's 4,
* which means it's an ECDIS variable, set by the user. On the left
* side it's just a straight number value comparison that will be
* provided for the right side.
*/
if (leftSide != null && leftSide.length() == 4) {
exp = new ECDISExpression(leftSide, val, opIndex, warehouse);
} else {
exp = new ValueExpression(leftSide, val, opIndex);
}
} catch (NumberFormatException nfe) {
/**
* This expression gets set up here for when a table value is
* compared against an ECDIS value.
*
* Turns out, there's never a need for the ColumnExpression because
* any time right side is text, it's actually referring to the value
* of the ECDIS External Attribute Name, which can be looked up and
* set as a variable.
*
* exp = new ColumnExpression(leftSide, rightSide, opIndex);
*/
// TODO Need to handle UNK and NULL!
double val = warehouse.getExternalAttribute(rightSide);
if (val < 0) {
// try to handle some string arguments
if (rightSide.equalsIgnoreCase("NULL")) {
exp = new StringExpression(leftSide, null, opIndex);
} else {
exp = new StringExpression(leftSide, rightSide, opIndex);
}
} else {
exp = new ValueExpression(leftSide, val, opIndex);
}
}
}
return exp;
}
/**
* Recursive parsing statement. Keys on Connectors (AND, OR) and builds
* Expressions based on those. Then looks for mini connectors (and, or) and
* builds on those. Of course, there might just be one expression here, one
* that is separated by an operator.
*
* @param source
* @return Expression tree
*/
protected Expression findExpression(String source) {
if (source != null && source.length() > 0) {
source = source.trim();
if (source.length() == 0) {
return null;
}
String leftSide = source;
String rightSide = null;
Connector op = findOp(leftSide);
if (op != null) {
rightSide = op.getRightSide(leftSide);
leftSide = leftSide.substring(0, op.sourceLoc);
Expression leftExpression = findExpression(leftSide);
Expression rightExpression = findExpression(rightSide);
if (leftExpression != null) {
op.addExpr(leftExpression);
}
if (rightExpression != null) {
op.addExpr(rightExpression);
}
return op;
}
// Look for mini ops
op = findMiniOp(leftSide);
if (op != null) {
rightSide = op.getRightSide(leftSide);
leftSide = leftSide.substring(0, op.sourceLoc);
Expression leftExpression = findExpression(leftSide);
Expression rightExpression = findExpression(rightSide);
if (leftExpression != null) {
op.addExpr(leftExpression);
}
if (rightExpression != null) {
op.addExpr(rightExpression);
}
return op;
}
// OK, here we are with the base expressions...
if (logger.isLoggable(Level.FINER)) {
logger.finer("need to break up: " + source);
}
return findMathOp(source);
}
return null;
}
/**
* Does the feature in row of fci pass the conditions of this expression.
*
* @param fci
* @param row
* @return true if row contents passes evaluation
*/
public boolean evaluate(FeatureClassInfo fci, int row) {
boolean ret = true;
StringBuffer reasoning = null;
if (logger.isLoggable(Level.FINE)) {
reasoning = new StringBuffer();
}
if (exp != null) {
ret = exp.evaluate(fci, row, reasoning);
}
if (reasoning != null) {
reasoning.append("\n--------");
logger.fine(reasoning.toString());
}
return ret;
}
/**
* This one is used by the CoverageTable. Does the feature in row of fci pass
* the conditions of this expression.
*
* @param fci
* @param row
* @return true if row passes evaluation
*/
public boolean evaluate(FeatureClassInfo fci, List<Object> row) {
boolean ret = true;
StringBuffer reasoning = null;
if (logger.isLoggable(Level.FINE)) {
reasoning = new StringBuffer();
logger.fine(toString());
}
if (exp != null) {
ret = exp.evaluate(fci, row, reasoning);
}
if (reasoning != null) {
reasoning.append("\n--------");
logger.fine(reasoning.toString());
}
return ret;
}
/**
* Connector class is the part of the expression that contains the logic
* operation, AND, OR, and and or.
*
* @author dietrick
*/
public static class Connector
implements Expression {
List<Expression> exp;
int op;
int sourceLoc;
public Connector(int op, int sLoc) {
this.op = op;
this.sourceLoc = sLoc;
}
public void addExpr(Expression expr) {
if (exp == null) {
exp = new LinkedList<Expression>();
}
if (expr != null) {
exp.add(expr);
updateColumnNamesIfNeeded();
}
}
protected void updateColumnNamesIfNeeded() {
String colName = null;
for (Expression e : exp) {
if (e instanceof CompareExpression) {
String cName = ((CompareExpression) e).colName;
if (cName != null) {
colName = cName;
break;
}
}
}
if (colName != null) {
for (Expression e : exp) {
if (e instanceof CompareExpression) {
if (((CompareExpression) e).colName == null) {
((CompareExpression) e).colName = colName;
break;
}
}
}
}
}
public String getRightSide(String source) {
switch (op) {
case NONE_CONN:
break;
case and_CONN:
case AND_CONN:
return source.substring(sourceLoc + 3).trim();
case or_CONN:
case OR_CONN:
return source.substring(sourceLoc + 2).trim();
default:
}
return null;
}
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning) {
boolean ret = false;
switch (op) {
case NONE_CONN:
break;
case or_CONN:
break;
case AND_CONN:
ret = true;
for (Expression e : exp) {
ret = e.evaluate(fci, row, reasoning);
if (!ret) {
break;
}
}
break;
case and_CONN:
break;
case OR_CONN:
for (Expression e : exp) {
ret = ret || e.evaluate(fci, row, reasoning);
if (ret) {
break;
}
}
break;
default:
}
if (reasoning != null) {
reasoning.append("\n-> " + toString() + ": evaluates " + ret);
}
return ret;
}
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning) {
boolean ret = false;
switch (op) {
case NONE_CONN:
break;
case AND_CONN:
case and_CONN:
ret = true;
for (Expression e : exp) {
ret = e.evaluate(fci, row, reasoning);
if (!ret) {
break;
}
}
break;
case or_CONN:
case OR_CONN:
for (Expression e : exp) {
ret = e.evaluate(fci, row, reasoning);
if (ret) {
break;
}
}
break;
default:
}
if (reasoning != null) {
reasoning.append("\n-> " + toString() + ": evaluates " + ret);
}
return ret;
}
public String toString() {
StringBuffer sb = new StringBuffer("Connector[");
boolean addConn = false;
String conn = " AND ";
if (op == OR_CONN) {
conn = " OR ";
}
for (Expression e : exp) {
if (addConn) {
sb.append(conn);
}
sb.append(e.toString());
addConn = true;
}
sb.append("]");
return sb.toString();
}
}
/**
* The ECDISExpression checks the warehouse for user set values when
* evaluating.
*
* @author dietrick
*/
public static class StringExpression
extends CompareExpression {
protected String val;
public StringExpression(String colName, String val, int op) {
super(colName, op);
if (val == null) {
val = "";
}
this.val = val;
}
/**
*
*/
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
List<Object> fcirow = new ArrayList<Object>();
try {
if (fci.getRow(fcirow, row)) {
if (colIndex < 0) {
if (reasoning != null) {
reasoning.append("\n col ").append(colName).append(" not found in FCI[").append(fci.columnNameString()).append("]");
}
logger.info("col " + colName + " not found in FCI[" + fci.columnNameString() + "]");
return false;
}
String realVal = fcirow.get(colIndex).toString().trim();
return test(realVal, val, reasoning);
} else {
if (reasoning != null) {
reasoning.append("\n Can't read row ").append(row);
}
}
} catch (FormatException fe) {
if (reasoning != null) {
reasoning.append("\n FormatException reading row ").append(row);
}
}
return false;
}
/**
* For ECDISExpressions, none of the arguments matter.
*/
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
// The columns aren't found
if (colIndex == -1) {
logger.finer("col " + colName + " not found in FCI[" + fci.columnNameString() + "]");
return false;
}
Object realVal = row.get(colIndex);
if (realVal == null) {
realVal = "";
}
return test(realVal.toString().trim(), val, reasoning);
}
/**
* The basic test for the operator, returning val1 op val2.
*
* @param val1 NOT NULL
* @param val2 NOT NULL
* @param buf
* @return true if operation passes
*/
protected boolean test(String val1, String val2, StringBuffer buf) {
boolean ret = false;
switch (op) {
case 1:
ret = val1.equals(val2);
break;
case 2:
ret = !val1.equals(val2);
break;
}
if (buf != null) {
String operation = null;
switch (op) {
case 1:
operation = (ret + "=" + val1 + "==" + val2);
break;
case 2:
operation = (ret + "=" + val1 + "!=" + val2);
break;
}
buf.append("\n " + toString() + ":" + operation);
}
return ret;
}
public String toString() {
return "StringExpression[" + colName + " " + ops[op] + " " + val + "]";
}
}
/**
* The ECDISExpression checks the warehouse for user set values when
* evaluating.
*
* @author dietrick
*/
public static class ECDISExpression
extends ValueExpression {
VPFAutoFeatureGraphicWarehouse warehouse = null;
public ECDISExpression(String colName, double val, int op, VPFAutoFeatureGraphicWarehouse warehouse) {
super(colName, val, op);
this.warehouse = warehouse;
}
/**
* For ECDISExpressions, none of the arguments matter.
*/
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning) {
return evaluate(reasoning);
}
/**
* For ECDISExpressions, none of the arguments matter.
*/
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning) {
return evaluate(reasoning);
}
public boolean evaluate(StringBuffer reasoning) {
double realVal = warehouse.getExternalAttribute(colName);
return test(realVal, val, reasoning);
}
public String toString() {
return "ECDISExpression[" + colName + " " + ops[op] + " " + val + "]";
}
}
/**
* The ValueExpression is a comparison of a FCI value to a numerical value.
*
* @author dietrick
*/
public static class ValueExpression
extends CompareExpression {
double val;
public ValueExpression(String colName, double val, int op) {
super(colName, op);
this.val = val;
}
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
List<Object> fcirow = new ArrayList<Object>();
try {
if (fci.getRow(fcirow, row)) {
if (colIndex < 0) {
if (reasoning != null) {
reasoning.append("\n col ").append(colName).append(" not found in FCI[").append(fci.columnNameString()).append("]");
}
return false;
}
Double realVal = Double.parseDouble(fcirow.get(colIndex).toString());
return test(realVal, val, reasoning);
} else {
if (reasoning != null) {
reasoning.append("\n Can't read row ").append(row);
}
}
} catch (FormatException fe) {
if (reasoning != null) {
reasoning.append("\n FormatException reading row ").append(row);
}
} catch (NumberFormatException nfe) {
if (reasoning != null) {
reasoning.append("\n NumberFormatException reading ").append(fcirow.get(colIndex));
}
}
return false;
}
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
try {
if (colIndex < 0) {
if (reasoning != null) {
reasoning.append("\n col ").append(colName).append(" not found in FCI[").append(fci.columnNameString()).append("]");
}
return false;
}
Double realVal = Double.parseDouble(row.get(colIndex).toString());
return test(realVal, val, reasoning);
} catch (NumberFormatException nfe) {
if (reasoning != null) {
reasoning.append("\n NumberFormatException reading ").append(row.get(colIndex));
}
}
return false;
}
public String toString() {
return "ValueExpression[" + colName + " " + ops[op] + " " + val + "]";
}
}
/**
* A ColumnExpression is the comparison of an FCI column value against
* another column value.
*
* @author dietrick
*/
public static class ColumnExpression
extends CompareExpression
implements Expression {
protected String otherColName;
protected int otherColIndex = -1;
public ColumnExpression(String colName, String otherName, int op) {
super(colName, op);
this.otherColName = otherName;
}
protected void setIndexes(FeatureClassInfo fci) {
this.fci = fci;
int columnCount = fci.getColumnCount();
colIndex = -1;
otherColIndex = -1;
for (int column = 0; column < columnCount; column++) {
if (fci.getColumnName(column).equalsIgnoreCase(colName)) {
colIndex = column;
}
if (fci.getColumnName(column).equalsIgnoreCase(otherColName)) {
otherColIndex = column;
}
}
}
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || otherColIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
// The columns aren't found
if (colIndex == -1 || otherColIndex == -1) {
logger.finer("col " + colName + " or " + otherColName + " not found in FCI[" + fci.columnNameString() + "]");
return false;
}
List<Object> fcirow = new ArrayList<Object>();
try {
if (fci.getRow(fcirow, row)) {
Double realVal1 = Double.parseDouble(fcirow.get(colIndex).toString());
Double realVal2 = Double.parseDouble(fcirow.get(otherColIndex).toString());
return test(realVal1, realVal2, reasoning);
}
} catch (FormatException fe) {
} catch (NumberFormatException nfe) {
}
return false;
}
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning) {
// Pre-cache column index so we don't have to do lookup for each entry.
if (colIndex == -1 || otherColIndex == -1 || this.fci != fci) {
setIndexes(fci);
}
// The columns aren't found
if (colIndex == -1 || otherColIndex == -1) {
logger.finer("col " + colName + " or " + otherColName + " not found in FCI[" + fci.columnNameString() + "]");
return false;
}
try {
Double realVal1 = Double.parseDouble(row.get(colIndex).toString());
Double realVal2 = Double.parseDouble(row.get(otherColIndex).toString());
return test(realVal1, realVal2, reasoning);
} catch (NumberFormatException nfe) {
}
return false;
}
public String toString() {
return "ValueExpression[" + colName + " " + ops[op] + " " + otherColName + "]";
}
}
public static abstract class CompareExpression
implements Expression {
protected int op;
protected FeatureClassInfo fci = null;
protected String colName;
protected int colIndex = -1;
protected CompareExpression(String colName, int op) {
this.colName = colName;
this.op = op;
}
protected void setIndexes(FeatureClassInfo fci) {
this.fci = fci;
colIndex = -1;
int columnCount = fci.getColumnCount();
for (int column = 0; column < columnCount; column++) {
if (fci.getColumnName(column).equalsIgnoreCase(colName)) {
colIndex = column;
break;
}
}
}
/**
* The basic test for the operator, returning val1 op val2.
*
* @param val1
* @param val2
* @param buf
* @return true of operation passes.
*/
protected boolean test(double val1, double val2, StringBuffer buf) {
boolean ret = false;
switch (op) {
case 1:
ret = val1 == val2;
break;
case 2:
ret = val1 != val2;
break;
case 3:
ret = val1 < val2;
break;
case 4:
ret = val1 > val2;
break;
case 5:
ret = val1 <= val2;
break;
case 6:
ret = val1 >= val2;
}
if (buf != null) {
String operation = null;
switch (op) {
case 1:
operation = (ret + "=" + val1 + "==" + val2);
break;
case 2:
operation = (ret + "=" + val1 + "!=" + val2);
break;
case 3:
operation = (ret + "=" + val1 + "<" + val2);
break;
case 4:
operation = (ret + "=" + val1 + ">" + val2);
break;
case 5:
operation = (ret + "=" + val1 + "<=" + val2);
break;
case 6:
operation = (ret + "=" + val1 + ">=" + val2);
}
buf.append("\n " + toString() + ":" + operation);
}
return ret;
}
}
/**
* The Expression interface allows for the recursive queries of Connectors
* and Value/CompareExpressions.
*
* @author dietrick
*/
public interface Expression {
public boolean evaluate(FeatureClassInfo fci, int row, StringBuffer reasoning);
public boolean evaluate(FeatureClassInfo fci, List<Object> row, StringBuffer reasoning);
}
public static void main(String[] args) {
new GeoSymAttExpression("mac=2 AND idsm=0 AND hdp>=msscand<ssdc AND isdm=0", new VPFAutoFeatureGraphicWarehouse());
}
}