/*
* Data Hub Service (DHuS) - For Space data distribution.
* Copyright (C) 2013,2014,2015,2016 GAEL Systems
*
* This file is part of DHuS software sources.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.gael.dhus.olingo.v1;
import org.apache.olingo.odata2.api.edm.EdmLiteral;
import org.apache.olingo.odata2.api.edm.EdmTyped;
import org.apache.olingo.odata2.api.uri.expression.BinaryExpression;
import org.apache.olingo.odata2.api.uri.expression.BinaryOperator;
import org.apache.olingo.odata2.api.uri.expression.ExpressionVisitor;
import org.apache.olingo.odata2.api.uri.expression.FilterExpression;
import org.apache.olingo.odata2.api.uri.expression.LiteralExpression;
import org.apache.olingo.odata2.api.uri.expression.MemberExpression;
import org.apache.olingo.odata2.api.uri.expression.MethodExpression;
import org.apache.olingo.odata2.api.uri.expression.MethodOperator;
import org.apache.olingo.odata2.api.uri.expression.OrderByExpression;
import org.apache.olingo.odata2.api.uri.expression.OrderExpression;
import org.apache.olingo.odata2.api.uri.expression.PropertyExpression;
import org.apache.olingo.odata2.api.uri.expression.SortOrder;
import org.apache.olingo.odata2.api.uri.expression.UnaryExpression;
import org.apache.olingo.odata2.api.uri.expression.UnaryOperator;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.Type;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
/**
* Implements the ExpressionVisitor interface to build SQL expressions from
* Olingo expression trees. visitFilterExpression builds the WHERE clause (not
* prefixed with the WHERE statement). visitOrderByExpression builds the ORDER
* BY clause (not prefixed with the ORDER BY statement). You must implement the
* <code>visitProperty</code> method which highly depends on the EDM.
*
* @see http://olingo.apache.org/doc/tutorials/
* Olingo_Tutorial_AdvancedRead_FilterVisitor.html
*/
public abstract class SQLVisitor implements ExpressionVisitor
{
private final DetachedCriteria criteria;
protected SQLVisitor(Class entity)
{
this.criteria = DetachedCriteria.forClass(entity);
}
/* Builds the WHERE clause (not prefixed with the WHERE statement). */
@Override
public Object visitFilterExpression(FilterExpression filter_expression,
String expression_string, Object expression)
{
if (expression != null)
{
criteria.add((Criterion) expression);
}
return criteria;
}
/* Builds the ORDER BY clause (not prefixed with the ORDER BY statement). */
@Override
public Object visitOrderByExpression(OrderByExpression order_expression,
String expression_string, List<Object> orders)
{
for (Object object: orders)
{
Order order = Order.class.cast(object);
criteria.addOrder(order);
}
return criteria;
}
/* Called for each fields in the $orderby param. */
@Override
public Object visitOrder(OrderExpression order_expression,
Object filter_result, SortOrder sort_order)
{
Order order;
String property = ((Member) filter_result).getName();
switch (sort_order)
{
case asc:
{
order = Order.asc(property);
break;
}
case desc:
{
order = Order.desc(property);
break;
}
default:
{
throw new UnsupportedOperationException("Unsupported order: " + sort_order);
}
}
return order;
}
/* Binary Operators. */
@Override
public Object visitBinary(BinaryExpression binary_expression,
BinaryOperator operator, Object left_side, Object right_side)
{
Criterion criterion;
switch (operator)
{
case EQ:
case NE:
case GT:
case GE:
case LT:
case LE:
{
criterion = getCriterionComparative(operator, left_side, right_side);
break;
}
case AND:
case OR:
{
Criterion left = (Criterion) left_side;
Criterion right = (Criterion) right_side;
criterion = getCriterionLogical(operator, left, right);
break;
}
default:
// Other operators are not supported for SQL Statements
throw new UnsupportedOperationException("Unsupported operator: " +
operator.toUriLiteral());
}
// return the binary statement
return criterion;
}
/* Unary operator. */
@Override
public Object visitUnary(UnaryExpression unary_expression,
UnaryOperator operator, Object operand)
{
switch (operator)
{
case MINUS:
{
if (operand instanceof Long)
{
return -((Long) operand);
}
else if (operand instanceof Double)
{
return -((Double) operand);
}
else
{
throw new UnsupportedOperationException("Invalid expression: " +
unary_expression.getUriLiteral());
}
}
case NOT:
{
return Restrictions.not((Criterion) operand);
}
default:
break;
}
throw new UnsupportedOperationException("Unsupported operator: " +
operator.toUriLiteral());
}
/* A constant. */
@Override
public Object visitLiteral(LiteralExpression literal, EdmLiteral edm_literal)
{
Object result;
Class type = edm_literal.getType().getDefaultType();
if (type.equals(Boolean.class))
{
result = Boolean.valueOf(edm_literal.getLiteral());
}
else if (type.equals(Byte.class) || type.equals(Short.class) ||
type.equals(Integer.class) || type.equals(Long.class))
{
result = Long.valueOf(edm_literal.getLiteral());
}
else if (type.equals(Double.class) || type.equals(BigDecimal.class))
{
result = Double.valueOf(edm_literal.getLiteral());
}
else if (type.equals(String.class))
{
result = edm_literal.getLiteral();
}
else if (type.equals(Calendar.class))
{
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
try
{
result = sdf1.parse(edm_literal.getLiteral());
}
catch (ParseException e)
{
try
{
result = sdf2.parse(edm_literal.getLiteral());
}
catch (ParseException e1)
{
throw new IllegalArgumentException("Invalid date format");
}
}
}
else
{
throw new IllegalArgumentException("Type " + edm_literal.getType() +
" is not supported by the service");
}
return result;
}
/* Translates to an SQL function. */
@Override
public Object visitMethod(MethodExpression method_expression,
MethodOperator method, List<Object> parameters)
{
Criterion criterion;
switch (method)
{
// String functions
case CONCAT:
{
criterion = Restrictions.sqlRestriction(
"CONCAT(?,?)",
new Object[]
{
parameters.get(0), parameters.get(1)
},
new Type[]
{
StandardBasicTypes.STRING, StandardBasicTypes.STRING
}
);
break;
}
case INDEXOF:
{
criterion = Restrictions.sqlRestriction(
"LOCATE(?,?)",
new Object[]
{
parameters.get(0), parameters.get(1)
},
new Type[]
{
StandardBasicTypes.STRING, StandardBasicTypes.STRING
}
);
break;
}
case LENGTH:
{
criterion = Restrictions.sqlRestriction(
"LENGTH(?)", parameters.get(0), StandardBasicTypes.STRING);
break;
}
case SUBSTRING:
{
criterion = Restrictions.sqlRestriction(
"SUBSTR(?,?)",
new Object[]
{
parameters.get(0), parameters.get(1)
},
new Type[]
{
StandardBasicTypes.STRING, StandardBasicTypes.STRING
}
);
break;
}
case TOUPPER:
{
criterion = Restrictions.sqlRestriction(
"UPPER(?)", parameters.get(0), StandardBasicTypes.STRING);
break;
}
case TOLOWER:
{
criterion = Restrictions.sqlRestriction(
"LOWER(?)", parameters.get(0), StandardBasicTypes.STRING);
break;
}
case TRIM:
{
criterion = Restrictions.sqlRestriction(
"TRIM(?)", parameters.get(0), StandardBasicTypes.STRING);
break;
}
case ENDSWITH:
case STARTSWITH:
{
criterion = getCriterionFunction(method, parameters.get(0), parameters.get(1));
break;
}
case SUBSTRINGOF:
{
criterion = getCriterionFunction(method, parameters.get(1), parameters.get(0));
break;
}
// Date functions
case DAY:
{
criterion = Restrictions.sqlRestriction("DAYOFMONTH(?)",
parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
case HOUR:
{
criterion = Restrictions.sqlRestriction(
"HOUR(?)", parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
case MINUTE:
{
criterion = Restrictions.sqlRestriction("MINUTE(?)",
parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
case MONTH:
{
criterion = Restrictions.sqlRestriction("MONTH(?)",
parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
case SECOND:
{
criterion = Restrictions.sqlRestriction("SECOND(?)",
parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
case YEAR:
{
criterion = Restrictions.sqlRestriction(
"YEAR(?)", parameters.get(0), StandardBasicTypes.TIMESTAMP);
break;
}
// Math functions
case CEILING:
{
criterion = Restrictions.sqlRestriction(
"CEILING(?)", parameters.get(0), StandardBasicTypes.DOUBLE);
break;
}
case FLOOR:
{
criterion = Restrictions.sqlRestriction(
"FLOOR (?)", parameters.get(0), StandardBasicTypes.DOUBLE);
break;
}
case ROUND:
{
criterion = Restrictions.sqlRestriction(
"ROUND(?)", parameters.get(0), StandardBasicTypes.DOUBLE);
break;
}
default:
throw new UnsupportedOperationException("Unsupported method: " +
method.toUriLiteral());
}
return criterion;
}
@Override
public Object visitMember(MemberExpression member_expression, Object path, Object property)
{
/* The property shall be handled inside visitProperty method in
* ODataSQLVisitor implementation */
return property;
}
/* Returns the field name corresponding to the given EDM type. */
@Override
public abstract Object visitProperty(PropertyExpression property_expression,
String uri_literal, EdmTyped edm_property);
private Criterion getCriterionComparative(BinaryOperator operator, Object left, Object right)
{
Criterion criterion = null;
if (left instanceof Member)
{
if (right instanceof Member)
{
// property <operator> property
String lvalue = ((Member) left).getName();
String rvalue = ((Member) right).getName();
switch (operator)
{
case EQ:
{
criterion = Restrictions.eqProperty(lvalue, rvalue);
break;
}
case NE:
{
criterion = Restrictions.neProperty(lvalue, rvalue);
break;
}
case GT:
{
criterion = Restrictions.gtProperty(lvalue, rvalue);
break;
}
case GE:
{
criterion = Restrictions.geProperty(lvalue, rvalue);
break;
}
case LT:
{
criterion = Restrictions.ltProperty(lvalue, rvalue);
break;
}
case LE:
{
criterion = Restrictions.leProperty(lvalue, rvalue);
break;
}
default:
throw new UnsupportedOperationException(
"Unsupported operation: " + operator.toUriLiteral());
}
}
else
{
// property <operator> literal
String property = ((Member) left).getName();
criterion = internalCriterionComparative(operator, property, right);
}
}
else if (right instanceof Member)
{
// literal <operator> property
String property = ((Member) right).getName();
criterion = internalCriterionComparative(operator, property, left);
}
else if (left instanceof Comparable)
{
// literal <operator> literal
Comparable comparable = (Comparable) left;
boolean bool;
int result = comparable.compareTo(right);
switch (operator)
{
case EQ:
{
bool = result == 0;
break;
}
case NE:
{
bool = result != 0;
break;
}
case GT:
{
bool = result > 0;
break;
}
case GE:
{
bool = result >= 0;
break;
}
case LT:
{
bool = result < 0;
break;
}
case LE:
{
bool = result <= 0;
break;
}
default:
throw new UnsupportedOperationException(
"Unsupported operation: " + operator.toUriLiteral());
}
if (bool)
{
criterion = Restrictions.sqlRestriction("0=0");
}
else
{
criterion = Restrictions.sqlRestriction("0<>0");
}
}
return criterion;
}
private Criterion internalCriterionComparative(
BinaryOperator operator, String property, Object value)
{
Criterion criterion;
switch (operator)
{
case EQ:
{
criterion = Restrictions.eq(property, value);
break;
}
case NE:
{
criterion = Restrictions.ne(property, value);
break;
}
case GT:
{
criterion = Restrictions.gt(property, value);
break;
}
case GE:
{
criterion = Restrictions.ge(property, value);
break;
}
case LT:
{
criterion = Restrictions.lt(property, value);
break;
}
case LE:
{
criterion = Restrictions.le(property, value);
break;
}
default:
throw new UnsupportedOperationException(
"Unsupported operation: " + operator.toUriLiteral());
}
return criterion;
}
private Criterion getCriterionLogical(BinaryOperator operator,
Criterion left, Criterion right)
{
Criterion criterion;
if (left == null && right == null)
{
criterion = null;
}
else if (left != null && right != null)
{
switch (operator)
{
case AND:
{
criterion = Restrictions.and(left, right);
break;
}
case OR:
{
criterion = Restrictions.or(left, right);
break;
}
default:
{
throw new UnsupportedOperationException(
"Unsupported operator: " + operator.toUriLiteral());
}
}
}
else if (left == null)
{
criterion = right;
}
else
{
criterion = left;
}
return criterion;
}
// WARNING: args[0] can be a Member BUT args[1] cannot be a Member!
private Criterion getCriterionFunction(MethodOperator method, Object... args)
{
Criterion criterion;
if (args[0] instanceof Member)
{
String property = ((Member) args[0]).getName();
switch (method)
{
case ENDSWITH:
{
String pattern = "%" + args[1];
criterion = Restrictions.like(property, pattern);
break;
}
case STARTSWITH:
{
String pattern = args[1] + "%";
criterion = Restrictions.like(property, pattern);
break;
}
case SUBSTRINGOF:
{
String pattern = "%" + args[1] + "%";
criterion = Restrictions.like(property, pattern);
break;
}
default:
{
throw new UnsupportedOperationException("Unsupported method: " +
method.toUriLiteral());
}
}
}
else
{
Type[] types =
{
StandardBasicTypes.STRING, StandardBasicTypes.STRING
};
switch (method)
{
case ENDSWITH:
{
Object[] parameters =
{
args[0], ("%" + args[1])
};
criterion = Restrictions.sqlRestriction("? LIKE ?", parameters, types);
break;
}
case STARTSWITH:
{
Object[] parameters =
{
args[0], (args[1] + "%")
};
criterion = Restrictions.sqlRestriction("? LIKE ?", parameters, types);
break;
}
case SUBSTRINGOF:
{
Object[] parameters =
{
args[0], ("%" + args[1] + "%")
};
criterion = Restrictions.sqlRestriction("? LIKE ?", parameters, types);
break;
}
default:
{
throw new UnsupportedOperationException("Unsupported method: " +
method.toUriLiteral());
}
}
}
return criterion;
}
protected static class Member
{
private final String name;
public Member(String name)
{
this.name = Objects.requireNonNull(name);
}
public String getName()
{
return name;
}
}
}