Value.java
/***************************************************************************
Copyright 2015 Emily Estes
Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
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 net.metanotion.sqltest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
interface Value {
public Object eval(Map<String,Value> environment);
public boolean compare(Object value, Map<String,Value> environment);
}
final class ObjectValue implements Value {
private final Object value;
public ObjectValue(final Object value) {
if(value==null) { throw new NullPointerException("Object value requires a non null object."); }
this.value = value;
}
@Override public Object eval(final Map<String,Value> environment) { return this.value; }
@Override public boolean compare(final Object val, final Map<String,Value> environment) {
return this.value.equals(value);
}
}
final class ErrorValue implements Value {
public static final ErrorValue INSTANCE = new ErrorValue();
@Override public Object eval(final Map<String,Value> env) { throw new RuntimeException("This value is an error value."); }
@Override public boolean compare(final Object val, final Map<String,Value> env) {
return (val instanceof Throwable);
}
@Override public String toString() { return "<Exception>"; }
}
final class AnyValue implements Value {
public static final AnyValue INSTANCE = new AnyValue();
private static final class Any {
/** Static analysis tools will flag this as a suspicious equals method. It *is* a suspicious method, but it's a
sentinel value to allow comparisons to occur against an "any" value when it's parent class is forced to produce
a value by calling the eval method. I can't think of a better way to solve this problem right now other than
disallowing <any> values to occur as anything but a "top level" literal value in the SQL test grammar. However
allowing users to specify that a field must have a value (but we don't care what the value is seems useful
enough to override the complaints from findbugs. */
@Override public boolean equals(final Object val) { return true; }
@Override public int hashCode() { throw new RuntimeException("<Any> stub value is unhashable."); }
};
private static final Any any = new Any();
@Override public Object eval(final Map<String,Value> env) { return any; }
@Override public boolean compare(final Object val, final Map<String,Value> env) {
return !(val instanceof Throwable);
}
@Override public String toString() { return "<Any>"; }
}
final class VariableValue implements Value {
private static final Logger logger = LoggerFactory.getLogger(VariableValue.class);
private final String variable;
public VariableValue(final String variable) { this.variable = variable; }
@Override public Value eval(final Map<String,Value> env) { return env.get(variable); }
@Override public boolean compare(final Object val, final Map<String,Value> env) {
logger.debug("Compare. Actual: {}", val);
final Value eVal = this.eval(env);
logger.debug("Compare. Expected: {}", eVal);
return eVal.compare(val, env);
}
}
final class NullValue implements Value {
public static final NullValue INSTANCE = new NullValue();
@Override public Object eval(final Map<String,Value> env) { return null; }
@Override public boolean compare(final Object value, final Map<String,Value> env) { return value == null; }
}
final class BooleanValue implements Value {
private final boolean result;
public BooleanValue(final boolean result) { this.result = result; }
@Override public Object eval(final Map<String,Value> env) { return this.result; }
@Override public boolean compare(final Object value, final Map<String,Value> env) {
if(!(value instanceof Boolean)) { return false; }
final Boolean b = (Boolean) value;
return result == b.booleanValue();
}
}
final class StringValue implements Value {
private final String result;
public StringValue(final String result) { this.result = result; }
@Override public Object eval(final Map<String,Value> env) { return this.result; }
@Override public boolean compare(final Object value, final Map<String,Value> env) { return this.result.equals(value); }
}
final class NumberValue implements Value {
private final BigDecimal result;
public static BigDecimal parse(final Number n) {
if(n instanceof BigDecimal) {
return (BigDecimal) n;
} else if(n instanceof Byte) {
return new BigDecimal((int) n.byteValue());
} else if(n instanceof Double) {
return new BigDecimal(n.doubleValue());
} else if(n instanceof Float) {
return new BigDecimal(n.floatValue());
} else if(n instanceof Integer) {
return new BigDecimal(n.intValue());
} else if(n instanceof Long) {
return new BigDecimal(n.longValue());
} else if(n instanceof Short) {
return new BigDecimal((int) n.shortValue());
} else {
throw new RuntimeException("Value isn't a usable number: " + n);
}
}
public NumberValue(final Number result) { this.result = parse(result); }
@Override public Object eval(final Map<String,Value> env) { return result; }
@Override public boolean compare(final Object val, final Map<String,Value> env) {
if(val instanceof Number) {
return result.compareTo(parse((Number) val)) == 0;
} else {
return false;
}
}
}
final class RowValue implements Value {
private static final Logger logger = LoggerFactory.getLogger(RowValue.class);
final Map<String,Value> row;
public RowValue(final Map<String,Value> row) { this.row = row; }
@Override public Map<String,Object> eval(final Map<String,Value> env) {
final HashMap<String,Object> result = new HashMap<>();
for(final Map.Entry<String, Value> e: row.entrySet()) {
result.put(e.getKey(), e.getValue().eval(env));
}
return result;
}
public static boolean compareMap(final Map<String,Object> expected,
final Map<String,Object> actual,
final Map<String,Value> env) {
logger.debug("Compare Maps");
for(final Map.Entry<String,Object> entry: expected.entrySet()) {
final Object e1 = entry.getValue();
final Object e = (e1 instanceof Number) ? NumberValue.parse((Number) e1) : e1;
final Object a1 = actual.get(entry.getKey());
final Object a = (a1 instanceof Number) ? NumberValue.parse((Number) a1) : a1;
logger.debug("Key: {}\n Expected {}.\n Actual {}\n", entry.getKey(), e, a);
if((e==null) && (a==null)) {
continue;
} else if(((e==null) && (a!=null)) || (!e.equals(a))) {
logger.debug(" Field comparison failed.");
return false;
}
}
return true;
}
@Override public boolean compare(final Object val, final Map<String,Value> env) {
logger.debug("Comparing this {} to {}", row, val);
final Map<String,Object> eVal = this.eval(env);
if(val instanceof Map) {
return compareMap(eVal, (Map<String,Object>) val, env);
} else {
return false;
}
}
}
final class ListValue implements Value {
private static final Logger logger = LoggerFactory.getLogger(ListValue.class);
final Iterable<Value> list;
public ListValue(final Iterable<Value> list) { this.list = list; }
@Override public Iterable<Object> eval(final Map<String,Value> env) {
final ArrayList<Object> result = new ArrayList<>();
for(final Value e: list) {
result.add(e.eval(env));
}
return result;
}
public static boolean compareIterator(final Iterable<Object> expected,
final Iterator<Object> actual,
final Map<String,Value> env) {
logger.debug("Compare iterators");
for(final Object e: expected) {
logger.debug(" List comparision. Iteration.");
if(!(actual.hasNext())) {
logger.debug("list too short.");
return false;
}
final Object a = actual.next();
logger.debug(" Expected element {}", e);
logger.debug(" Actual element {}", a);
if((e == null) && (a != null)) {
logger.debug("Expected was null, but compared value wasn't.");
return false;
}
if((e instanceof Map) && (a instanceof Map)) {
if(!RowValue.compareMap((Map) e, (Map) a, env)) {
logger.debug("List Row to Row comparison failed.");
return false;
}
} else if((e==null) && (a==null)) {
continue;
} else if(!e.equals(a)) {
return false;
}
}
if(actual.hasNext()) {
logger.debug("list was too long.");
return false;
}
return true;
}
@Override public boolean compare(final Object val, final Map<String,Value> env) {
logger.debug("Actual {}", val);
final Iterable<Object> eVal = this.eval(env);
logger.debug("Expected {}", eVal);
if(val instanceof Iterable) {
return compareIterator(eVal, ((Iterable<Object>) val).iterator(), env);
} else {
return false;
}
}
}