blob: aeb9b940e3c786e27e5e519da4c3908c2730bebe [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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 org.apache.geode.cache.query.internal.types;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.geode.InternalGemFireError;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.Support;
import org.apache.geode.cache.query.internal.Undefined;
import org.apache.geode.cache.query.internal.parse.OQLLexerTokenTypes;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.pdx.internal.EnumInfo.PdxInstanceEnumInfo;
import org.apache.geode.pdx.internal.PdxInstanceEnum;
import org.apache.geode.pdx.internal.PdxString;
/**
* Utilities for casting and comparing values of possibly differing types, testing and cloning query
* literals.
*
* @version $Revision: 1.1 $
*/
public class TypeUtils implements OQLLexerTokenTypes {
@Immutable
protected static final List<Class> _numericPrimitiveClasses =
Collections.unmodifiableList(Arrays.asList(
new Class[] {Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE}));
@Immutable
protected static final List<Class> _numericWrapperClasses =
Collections.unmodifiableList(Arrays.asList(
new Class[] {Byte.class, Short.class, Integer.class, Long.class, Float.class,
Double.class}));
/**
* Enum to execute comparisons based on types.
*/
private enum ComparisonStrategy {
TEMPORAL {
@Override
public Boolean execute(Object object1, Object object2, int comparator)
throws ClassCastException {
return applyComparator(getTemporalComparator().compare(object1, object2), comparator);
}
},
NUMERIC {
@Override
public Boolean execute(Object object1, Object object2, int comparator)
throws ClassCastException {
return applyComparator(getNumericComparator().compare(object1, object2), comparator);
}
},
BOOLEAN {
@Override
public Boolean execute(Object object1, Object object2, int comparator)
throws TypeMismatchException {
return booleanCompare(object1, object2, comparator);
}
},
COMPARABLE {
@Override
public Boolean execute(Object object1, Object object2, int comparator)
throws ClassCastException {
return applyComparator(((Comparable) object1).compareTo(object2), comparator);
}
},
ARBITRARY {
@Override
public Boolean execute(Object object1, Object object2, int comparator) {
if (comparator == TOK_EQ) {
return object1.equals(object2);
} else {
return !object1.equals(object2);
}
}
};
protected Boolean applyComparator(int temporalResult, int comparator)
throws IllegalArgumentException {
switch (comparator) {
case TOK_EQ:
return temporalResult == 0;
case TOK_LT:
return temporalResult < 0;
case TOK_LE:
return temporalResult <= 0;
case TOK_GT:
return temporalResult > 0;
case TOK_GE:
return temporalResult >= 0;
case TOK_NE:
return temporalResult != 0;
default:
throw new IllegalArgumentException(String.format("Unknown operator: %s",
Integer.valueOf(comparator)));
}
}
public static ComparisonStrategy get(Class object1Class, Class object2Class, int comparator)
throws TypeMismatchException {
if (isAssignableFrom(object1Class, Date.class)
&& isAssignableFrom(object1Class, Date.class)) {
return TEMPORAL;
} else if (object1Class != object2Class && (isAssignableFrom(object1Class, Number.class)
&& isAssignableFrom(object2Class, Number.class))) {
return NUMERIC;
} else if (isAssignableFrom(object1Class, Boolean.class)
|| isAssignableFrom(object2Class, Boolean.class)) {
return BOOLEAN;
} else if (isAssignableFrom(object1Class, Comparable.class)
&& isAssignableFrom(object2Class, Comparable.class)) {
return COMPARABLE;
} else if ((comparator == TOK_EQ) || (comparator == TOK_NE)) {
return ARBITRARY;
} else {
throw new TypeMismatchException(
String.format(
"Unable to use a relational comparison operator to compare an instance of class ' %s ' with an instance of ' %s '",
new Object[] {object1Class.getName(), object2Class.getName()}));
}
}
/**
* Executes the comparison strategy.
*
*/
public abstract Boolean execute(Object object1, Object object2, int comparator)
throws TypeMismatchException, ClassCastException;
}
/* Common Types */
/** ObjectType for Object.class */
@Immutable
public static final ObjectType OBJECT_TYPE = new ObjectTypeImpl(Object.class);
/** prevent instantiation */
private TypeUtils() {}
/**
* Creates an instance of {@link org.apache.geode.cache.query.internal.types.TemporalComparator}.
*
* @return Comparator for mixed comparisons between instances of java.util.Date, java.sql.Date,
* java.sql.Time, and java.sql.Timestamp.
* @see org.apache.geode.cache.query.internal.types.TemporalComparator
*/
public static Comparator getTemporalComparator() {
return new TemporalComparator();
}
/**
* Creates an instance of {@link org.apache.geode.cache.query.internal.types.NumericComparator}.
*
* @return Comparator for mixed comparisons between numbers.
* @see org.apache.geode.cache.query.internal.types.NumericComparator
*/
public static Comparator getNumericComparator() {
return new NumericComparator();
}
/**
* Creates an instance of
* {@link org.apache.geode.cache.query.internal.types.ExtendedNumericComparator}.
*
* @return A general comparator that compares different numeric types for equality.
* @see org.apache.geode.cache.query.internal.types.ExtendedNumericComparator
*/
public static Comparator getExtendedNumericComparator() {
return new ExtendedNumericComparator();
}
/**
* Verify that type-cast will work, or else throw an informative exception.
*
* @return the castTarget
* @throws InternalGemFireError if cast will fail
*/
public static Object checkCast(Object castTarget, Class castClass) {
if (castTarget == null) {
return null; // null can be cast to anything
}
if (!castClass.isInstance(castTarget)) {
throw new InternalGemFireError(String.format("expected instance of %s but was %s",
new Object[] {castClass.getName(), castTarget.getClass().getName()}));
}
return castTarget;
}
/**
* Given an arbitrary object, return the value that should be used as the key for a particular
* index.
*
* @param obj the object to evaluate.
* @return the value that should be used as the key for an index.
* @throws TypeMismatchException if the object type is not supported to be used as an index.
*/
public static Object indexKeyFor(Object obj) throws TypeMismatchException {
if (obj == null) {
return null;
}
if (obj instanceof Byte) {
return Integer.valueOf(((Byte) obj).intValue());
}
if (obj instanceof Short) {
return Integer.valueOf(((Short) obj).intValue());
}
// Ketan : Added later. Indexes should be created
// if the IndexedExpr implements Comparable interface.
if (obj instanceof Comparable) {
if (obj instanceof Enum) {
obj = new PdxInstanceEnum((Enum<?>) obj);
}
return obj;
}
throw new TypeMismatchException(String.format("Indexes are not supported for type ' %s '",
obj.getClass().getName()));
}
/**
* Determines if the Class represented by the first parameter is either the same as, or is a
* superclass or superinterface of, the class or interface represented by the second parameter.
*
* @param srcType the class to be checked.
* @param destType the class to be checked against.
* @return the {@code boolean} value indicating whether objects of the type {@code srcType} can be
* assigned to objects of type {@code destType} .
*/
protected static boolean isAssignableFrom(Class srcType, Class destType) {
return destType.isAssignableFrom(srcType);
}
/**
* Determines if the Class represented by the first parameter can be safely converted to the Class
* represented by the second parameter.
*
* @param srcType the source class to be converted.
* @param destType the target class to be converted to.
* @return the {@code boolean} value indicating whether objects of the type {@code srcType} can be
* converted to objects of type {@code destType} .
*/
public static boolean isTypeConvertible(Class srcType, Class destType) {
// handle null: if srcType is null, then it represents
// a null runtime value, and for our purposes it is type
// convertible to any type that is assignable from Object.class
if (srcType == null) {
// TODO: This should always be true as Object is assignable from everything, is the root type
// of all Java classes. Object.class is always assignable from every other Java class.
return isAssignableFrom(destType, Object.class);
}
// check to see if the classes are assignable
if (isAssignableFrom(srcType, destType)) {
return true;
}
// handle booleans: are we going from a wrapper Boolean
// to a primitive boolean or vice-versa?
if ((srcType == Boolean.TYPE || srcType == Boolean.class)
&& (destType == Boolean.TYPE || destType == Boolean.class)) {
return true;
}
// a numeric primitive or wrapper can be converted to
// the same wrapper or a same or wider primitive.
// handle chars specially
int i = _numericPrimitiveClasses.indexOf(srcType);
if (i < 0) {
i = _numericWrapperClasses.indexOf(srcType);
}
int destP = _numericPrimitiveClasses.indexOf(destType);
int destW = -1;
if (destP < 0) {
destW = _numericWrapperClasses.indexOf(destType);
}
// same size wrapper
if (i >= 0 && destW == i) {
return true;
}
// same or wider primitive
if (i >= 0 && destP >= i) {
return true;
}
// chars
if (srcType == Character.class || srcType == Character.TYPE) {
// chars: same size wrapper/primitive
if (destType == Character.class || destType == Character.TYPE) {
return true;
}
}
// no other possibilities
return false;
}
/**
* Determines if the all classes represented by the first parameter can be safely converted to the
* classes represented by the second parameter.
*
* @param srcTypes the source classes to be converted.
* @param destTypes the target classes to be converted to.
* @return the {@code boolean} value indicating whether all objects of the first array can be
* converted to objects of the second array.
*/
public static boolean areTypesConvertible(Class[] srcTypes, Class[] destTypes) {
Support.assertArg(srcTypes.length == destTypes.length,
"Arguments 'srcTypes' and 'destTypes' must be of same length");
for (int i = 0; i < srcTypes.length; i++) {
if (!isTypeConvertible(srcTypes[i], destTypes[i])) {
return false;
}
}
return true;
}
/**
* Return the {@link org.apache.geode.cache.query.types.ObjectType} corresponding to the specified
* Class.
*
* @return the internal ObjectType implementation that represents the specified class.
*/
public static ObjectType getObjectType(Class cls) {
if (cls == Object.class) {
return OBJECT_TYPE;
}
if (Collection.class.isAssignableFrom(cls)) {
// we don't have element type info here
return new CollectionTypeImpl(cls, OBJECT_TYPE);
}
if (cls.isArray()) {
return new CollectionTypeImpl(cls, getObjectType(cls.getComponentType()));
}
if (Region.class.isAssignableFrom(cls)) {
// we don't have access to the region itself for element type
return new CollectionTypeImpl(cls, OBJECT_TYPE);
}
if (Map.class.isAssignableFrom(cls)) {
return new MapTypeImpl(cls, OBJECT_TYPE, OBJECT_TYPE);
}
// if it's a struct we have no field info, so just return an ObjectTypeImpl
return new ObjectTypeImpl(cls);
}
/**
* Return a fixed {@link org.apache.geode.cache.query.types.ObjectType} representing a Region
* Entry.
*
* @return the internal ObjectType implementation that represents a the region entry class.
*/
public static ObjectType getRegionEntryType(Region rgn) {
// just use an ObjectType for now
return new ObjectTypeImpl(Region.Entry.class);
}
/**
* Compare two booleans.
*
* @return a boolean indicating the result of applying the comparison operator on the arguments.
* @throws TypeMismatchException When either one of the arguments is not a Boolean, or the
* comparison operator is not supported.
*/
protected static boolean booleanCompare(Object obj1, Object obj2, int compOp)
throws TypeMismatchException {
if (!(obj1 instanceof Boolean) || !(obj2 instanceof Boolean)) {
throw new TypeMismatchException(
"Booleans can only be compared with booleans");
}
if (compOp == TOK_EQ) {
return obj1.equals(obj2);
} else if (compOp == TOK_NE) {
return !obj1.equals(obj2);
} else {
throw new TypeMismatchException(
"Boolean values can only be compared with = or <>");
}
}
/**
*
* @return a Boolean, or null if UNDEFINED.
*/
private static Boolean nullCompare(Object obj1, Object obj2, int compOp) {
Boolean result = null;
switch (compOp) {
case TOK_EQ: {
if (obj1 == null) {
result = Boolean.valueOf(obj2 == null);
} else { // obj1 is not null obj2 must be
result = Boolean.FALSE;
}
break;
}
case TOK_NE: {
if (obj1 == null) {
result = Boolean.valueOf(obj2 != null);
} else { // obj1 is not null so obj2 must be
result = Boolean.TRUE;
}
break;
}
}
return result;
}
/**
* Compares two objects using the operator
*
* @return boolean;<br>
* {@link Undefined} if either of the operands is {@link Undefined} or if either of the
* operands is Null and operator is other than == or !=
*/
public static Object compare(Object obj1, Object obj2, int compOp) throws TypeMismatchException {
// Check for nulls first.
if (obj1 == null || obj2 == null) {
Boolean result = nullCompare(obj1, obj2, compOp);
if (result == null) {
return QueryService.UNDEFINED;
}
return result;
}
// if either object is UNDEFINED, result is UNDEFINED
if (obj1 == QueryService.UNDEFINED || obj2 == QueryService.UNDEFINED) {
if (compOp == TOK_NE && !(obj1 == QueryService.UNDEFINED && obj2 == QueryService.UNDEFINED)) {
return true;
} else if (compOp == TOK_EQ && obj1.equals(obj2)) {
return true;
} else {
return QueryService.UNDEFINED;
}
}
// Prepare pdx instances if needed.
if (obj1 instanceof PdxInstanceEnumInfo && obj2 instanceof Enum) {
obj2 = new PdxInstanceEnum((Enum<?>) obj2);
} else if (obj1 instanceof Enum && obj2 instanceof PdxInstanceEnumInfo) {
obj1 = new PdxInstanceEnum((Enum<?>) obj1);
}
if (obj1 instanceof PdxString && obj2 instanceof String) {
obj2 = new PdxString((String) obj2);
} else if (obj1 instanceof String && obj2 instanceof PdxString) {
obj1 = new PdxString((String) obj1);
}
try {
ComparisonStrategy strategy =
ComparisonStrategy.get(obj1.getClass(), obj2.getClass(), compOp);
return strategy.execute(obj1, obj2, compOp);
} catch (ClassCastException | TypeMismatchException e) {
// if a ClassCastException or TypeMismatchException was thrown and the operator is equals
// or not equals, then override and return true or false.
if (compOp == TOK_EQ) {
return Boolean.FALSE;
}
if (compOp == TOK_NE) {
return Boolean.TRUE;
}
if (isAssignableFrom(e.getClass(), ClassCastException.class)) {
throw new TypeMismatchException(
String.format("Unable to compare object of type ' %s ' with object of type ' %s '",
new Object[] {obj1.getClass().getName(), obj2.getClass().getName()}),
e);
} else {
throw e;
}
}
}
}