/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.translator.odata;
import static org.teiid.language.SQLConstants.Reserved.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.UriBuilder;
import org.teiid.language.*;
import org.teiid.language.SQLConstants.Tokens;
import org.teiid.language.SortSpecification.Ordering;
import org.teiid.language.visitor.HierarchyVisitor;
import org.teiid.metadata.Column;
import org.teiid.metadata.FunctionMethod;
import org.teiid.metadata.RuntimeMetadata;
import org.teiid.metadata.Table;
import org.teiid.translator.SourceSystemFunctions;
import org.teiid.translator.TranslatorException;
public class ODataSQLVisitor extends HierarchyVisitor {
private static Map<String, String> infixFunctions = new HashMap<String, String>();
static {
infixFunctions.put("%", "mod");//$NON-NLS-1$ //$NON-NLS-2$
infixFunctions.put("+", "add");//$NON-NLS-1$ //$NON-NLS-2$
infixFunctions.put("-", "sub");//$NON-NLS-1$ //$NON-NLS-2$
infixFunctions.put("*", "mul");//$NON-NLS-1$ //$NON-NLS-2$
infixFunctions.put("/", "div");//$NON-NLS-1$ //$NON-NLS-2$
}
protected ArrayList<TranslatorException> exceptions = new ArrayList<TranslatorException>();
protected QueryExpression command;
protected ODataExecutionFactory executionFactory;
protected RuntimeMetadata metadata;
protected ArrayList<Column> selectColumns = new ArrayList<Column>();
protected StringBuilder filter = new StringBuilder();
private EntitiesInQuery entities = new EntitiesInQuery();
private Integer skip;
private Integer top;
private StringBuilder orderBy = new StringBuilder();
private boolean count = false;
public Column[] getSelect(){
return this.selectColumns.toArray(new Column[this.selectColumns.size()]);
}
public boolean isCount() {
return this.count;
}
public boolean isKeyLookup() {
return this.entities.isKeyLookup();
}
public Table getEnityTable() {
return this.entities.getFinalEntity();
}
public String getEnitityURL() {
StringBuilder url = new StringBuilder();
this.entities.append(url);
return url.toString();
}
public String buildURL() {
StringBuilder url = new StringBuilder();
this.entities.append(url);
if (this.count) {
url.append("/$count"); //$NON-NLS-1$
}
UriBuilder uriBuilder = UriBuilder.fromPath(url.toString());
if (this.filter.length() > 0) {
uriBuilder.queryParam("$filter", this.filter.toString()); //$NON-NLS-1$
}
if (this.orderBy.length() > 0) {
uriBuilder.queryParam("$orderby", this.orderBy.toString()); //$NON-NLS-1$
}
if (!this.selectColumns.isEmpty()) {
LinkedHashSet<String> select = new LinkedHashSet<String>();
for (Column column:this.selectColumns) {
select.add(getColumnName(column));
}
StringBuilder sb = new StringBuilder();
Iterator<String> it = select.iterator();
while(it.hasNext()) {
sb.append(it.next());
if (it.hasNext()) {
sb.append(Tokens.COMMA);
}
}
uriBuilder.queryParam("$select", sb.toString()); //$NON-NLS-1$
}
if (this.skip != null) {
uriBuilder.queryParam("$skip", this.skip); //$NON-NLS-1$
}
if (this.top != null) {
uriBuilder.queryParam("$top", this.top); //$NON-NLS-1$
}
//if (!this.count) {
// uriBuilder.queryParam("$format", "atom"); //$NON-NLS-1$ //$NON-NLS-2$
//}
URI uri = uriBuilder.build();
return uri.toString();
}
public ODataSQLVisitor(ODataExecutionFactory executionFactory,
RuntimeMetadata metadata) {
this.executionFactory = executionFactory;
this.metadata = metadata;
}
@Override
public void visit(Comparison obj) {
Expression left = obj.getLeftExpression();
if(!this.executionFactory.supportsOdataBooleanFunctionsWithComparison()
&& left instanceof Function
&& "boolean".equals(((Function)left).getMetadataObject().getOutputParameter().getRuntimeType())) {
visitComparisonWithBooleanFunction(obj);
// early exit
return;
}
append(left);
this.filter.append(Tokens.SPACE);
switch(obj.getOperator()) {
case EQ:
this.filter.append("eq"); //$NON-NLS-1$
break;
case NE:
this.filter.append("ne"); //$NON-NLS-1$
break;
case LT:
this.filter.append("lt"); //$NON-NLS-1$
break;
case LE:
this.filter.append("le"); //$NON-NLS-1$
break;
case GT:
this.filter.append("gt"); //$NON-NLS-1$
break;
case GE:
this.filter.append("ge"); //$NON-NLS-1$
break;
}
this.filter.append(Tokens.SPACE);
appendRightComparison(obj);
}
public void visitComparisonWithBooleanFunction(Comparison obj) {
boolean truthiness = SQLConstants.Reserved.TRUE.equals(obj.getRightExpression().toString());
boolean isNot = !truthiness;
switch(obj.getOperator()) {
case EQ:
break;
case NE:
isNot = !isNot;
break;
default:
this.exceptions.add(new TranslatorException(
ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17018, ((Function)obj.getLeftExpression()).getName())));
}
if(isNot) {
// can't use a Not object, because it requires a Condition inside,
// and we don't have support for generic unary conditions
this.filter.append(NOT)
.append(Tokens.SPACE)
.append(Tokens.LPAREN);
append(obj.getLeftExpression());
this.filter.append(Tokens.RPAREN);
} else {
append(obj.getLeftExpression());
}
}
protected void appendRightComparison(Comparison obj) {
append(obj.getRightExpression());
}
@Override
public void visit(AndOr obj) {
String opString = obj.getOperator().name().toLowerCase();
appendNestedCondition(obj, obj.getLeftCondition());
this.filter.append(Tokens.SPACE)
.append(opString)
.append(Tokens.SPACE);
appendNestedCondition(obj, obj.getRightCondition());
}
protected void appendNestedCondition(AndOr parent, Condition condition) {
if (condition instanceof AndOr) {
AndOr nested = (AndOr)condition;
if (nested.getOperator() != parent.getOperator()) {
this.filter.append(Tokens.LPAREN);
append(condition);
this.filter.append(Tokens.RPAREN);
return;
}
}
append(condition);
}
@Override
public void visit(ColumnReference obj) {
this.filter.append(obj.getMetadataObject().getName());
}
protected boolean isInfixFunction(String function) {
return infixFunctions.containsKey(function);
}
@Override
public void visit(Function obj) {
if (this.executionFactory.getFunctionModifiers().containsKey(obj.getName())) {
this.executionFactory.getFunctionModifiers().get(obj.getName()).translate(obj);
}
String name = obj.getName();
List<Expression> args = obj.getParameters();
if(isInfixFunction(name)) {
this.filter.append(Tokens.LPAREN);
if(args != null) {
for(int i=0; i<args.size(); i++) {
append(args.get(i));
if(i < (args.size()-1)) {
this.filter.append(Tokens.SPACE);
this.filter.append(infixFunctions.get(name));
this.filter.append(Tokens.SPACE);
}
}
}
this.filter.append(Tokens.RPAREN);
}
else {
FunctionMethod method = obj.getMetadataObject();
if (name.startsWith(method.getCategory())) {
name = name.substring(method.getCategory().length()+1);
}
this.filter.append(name)
.append(Tokens.LPAREN);
if (args != null && args.size() != 0) {
if (SourceSystemFunctions.ENDSWITH.equalsIgnoreCase(name)) {
append(args.get(1));
this.filter.append(Tokens.COMMA);
append(args.get(0));
} else {
for (int i = 0; i < args.size(); i++) {
append(args.get(i));
if (i < args.size()-1) {
this.filter.append(Tokens.COMMA);
}
}
}
}
this.filter.append(Tokens.RPAREN);
}
}
@Override
public void visit(NamedTable obj) {
this.entities.addEntity(obj.getMetadataObject());
}
@Override
public void visit(IsNull obj) {
if (obj.isNegated()) {
this.filter.append(NOT.toLowerCase()).append(Tokens.LPAREN);
}
appendNested(obj.getExpression());
this.filter.append(Tokens.SPACE);
this.filter.append("eq").append(Tokens.SPACE); //$NON-NLS-1$
this.filter.append(NULL.toLowerCase());
if (obj.isNegated()) {
this.filter.append(Tokens.RPAREN);
}
}
private void appendNested(Expression ex) {
boolean useParens = ex instanceof Condition;
if (useParens) {
this.filter.append(Tokens.LPAREN);
}
append(ex);
if (useParens) {
this.filter.append(Tokens.RPAREN);
}
}
@Override
public void visit(Join obj) {
// joins are not used currently
if (obj.getLeftItem() instanceof NamedTable && obj.getRightItem() instanceof NamedTable) {
this.entities.addEntity(((NamedTable)obj.getLeftItem()).getMetadataObject());
this.entities.addEntity(((NamedTable)obj.getRightItem()).getMetadataObject());
obj.setCondition(buildEntityKey(obj.getCondition()));
visitNode(obj.getCondition());
}
else {
visitNode(obj.getLeftItem());
visitNode(obj.getRightItem());
visitNode(obj.getCondition());
}
}
@Override
public void visit(Limit obj) {
if (obj.getRowOffset() != 0) {
this.skip = new Integer(obj.getRowOffset());
}
if (obj.getRowLimit() != 0) {
this.top = new Integer(obj.getRowLimit());
}
}
@Override
public void visit(Literal obj) {
this.executionFactory.convertToODataInput(obj, this.filter);
}
@Override
public void visit(Not obj) {
this.filter.append(NOT)
.append(Tokens.SPACE)
.append(Tokens.LPAREN);
append(obj.getCriteria());
this.filter.append(Tokens.RPAREN);
}
@Override
public void visit(OrderBy obj) {
append(obj.getSortSpecifications());
}
@Override
public void visit(SortSpecification obj) {
if (this.orderBy.length() > 0) {
this.orderBy.append(Tokens.COMMA);
}
ColumnReference column = (ColumnReference)obj.getExpression();
this.orderBy.append(column.getMetadataObject().getName());
// default is ascending
if (obj.getOrdering() == Ordering.DESC) {
this.orderBy.append(Tokens.SPACE).append(DESC.toLowerCase());
}
}
@Override
public void visit(Select obj) {
visitNodes(obj.getFrom());
obj.setWhere(buildEntityKey(obj.getWhere()));
visitNode(obj.getWhere());
visitNode(obj.getOrderBy());
visitNode(obj.getLimit());
visitNodes(obj.getDerivedColumns());
}
protected Condition buildEntityKey(Condition obj) {
List<Condition> crits = LanguageUtil.separateCriteriaByAnd(obj);
if (!crits.isEmpty()) {
boolean modified = false;
for(Iterator<Condition> iter = crits.iterator(); iter.hasNext();) {
Condition crit = iter.next();
if (crit instanceof Comparison) {
Comparison left = (Comparison) crit;
boolean leftAdded = this.entities.addEnityKey(left);
if (leftAdded) {
iter.remove();
modified = true;
}
}
}
if (this.entities.valid() && modified) {
return LanguageUtil.combineCriteria(crits);
}
}
return obj;
}
@Override
public void visit(DerivedColumn obj) {
if (obj.getExpression() instanceof ColumnReference) {
Column column = ((ColumnReference)obj.getExpression()).getMetadataObject();
String joinColumn = column.getProperty(ODataMetadataProcessor.JOIN_COLUMN, false);
if (joinColumn != null && Boolean.valueOf(joinColumn)) {
this.exceptions.add(new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17006, column.getName())));
}
this.selectColumns.add(column);
}
else if (obj.getExpression() instanceof AggregateFunction) {
AggregateFunction func = (AggregateFunction)obj.getExpression();
if (func.getName().equalsIgnoreCase("COUNT")) { //$NON-NLS-1$
this.count = true;
}
else {
this.exceptions.add(new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17007, func.getName())));
}
}
else {
this.exceptions.add(new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17008)));
}
}
private String getColumnName(Column column) {
String columnName = column.getName();
// Check if this is a embedded column, if it is then only
// add the parent type
String columnGroup = column.getProperty(ODataMetadataProcessor.COLUMN_GROUP, false);
if (columnGroup != null) {
columnName = columnGroup;
}
return columnName;
}
public void append(LanguageObject obj) {
visitNode(obj);
}
protected void append(List<? extends LanguageObject> items) {
if (items != null && items.size() != 0) {
for (int i = 0; i < items.size(); i++) {
append(items.get(i));
}
}
}
protected void append(LanguageObject[] items) {
if (items != null && items.length != 0) {
for (int i = 0; i < items.length; i++) {
append(items[i]);
}
}
}
static class Entity {
Table table;
Map<Column, Literal> pkValues = new LinkedHashMap<Column, Literal>();
boolean hasValidKey = false;
List<Object[]> relations = new ArrayList<Object[]>();
public Entity(Table t) {
this.table = t;
}
public void addKeyValue(Column column, Literal value) {
addKeyValue(column, value, true);
}
private void addKeyValue(Column column, Literal value, boolean walkRelations) {
// add in self key
this.pkValues.put(column, value);
if (walkRelations) {
// See any other relations exist.
for (Object[] relation:this.relations) {
if (column.equals(relation[0])){
((Entity)relation[2]).addKeyValue((Column)relation[1], value, false);
}
}
}
for (Column col:this.table.getPrimaryKey().getColumns()) {
if (this.pkValues.get(col) == null) {
return;
}
}
this.hasValidKey = true;
}
public boolean hasValidKey() {
return this.hasValidKey;
}
public void addRelation(Column self, Column other, Entity otherEntity) {
this.relations.add(new Object[] {self, other, otherEntity});
}
}
class EntitiesInQuery {
ArrayList<Entity> entities = new ArrayList<ODataSQLVisitor.Entity>();
public void append(StringBuilder url) {
if (this.entities.size() == 1) {
addEntityToURL(url, this.entities.get(0));
}
else if (this.entities.size() > 1) {
for (int i = 0; i < this.entities.size()-1; i++) {
addEntityToURL(url, this.entities.get(i));
url.append("/"); //$NON-NLS-1$
}
addEntityToURL(url, this.entities.get(this.entities.size()-1));
}
}
public boolean isKeyLookup() {
return this.entities.get(this.entities.size()-1).hasValidKey();
}
public Table getFinalEntity() {
return this.entities.get(this.entities.size()-1).table;
}
private void addEntityToURL(StringBuilder url, Entity entity) {
url.append(entity.table.getName());
if (entity.hasValidKey()) {
boolean useNames = entity.pkValues.size() > 1; // multi-key PK, use the name value pairs
url.append("("); //$NON-NLS-1$
boolean firstKey = true;
for (Column c:entity.pkValues.keySet()) {
if (firstKey) {
firstKey = false;
}
else {
url.append(Tokens.COMMA);
}
if (useNames) {
url.append(c.getName()).append(Tokens.EQ);
}
ODataSQLVisitor.this.executionFactory.convertToODataInput(entity.pkValues.get(c), url);
}
url.append(")"); //$NON-NLS-1$
}
}
public void addEntity(Table table) {
Entity entity = new Entity(table);
this.entities.add(entity);
}
private Entity getEntity(Table table) {
for (Entity e:this.entities) {
if (e.table.equals(table)) {
return e;
}
}
return null;
}
private boolean addEnityKey(Comparison obj) {
if (obj.getOperator().equals(Comparison.Operator.EQ)) {
if (obj.getLeftExpression() instanceof ColumnReference && obj.getRightExpression() instanceof Literal) {
ColumnReference columnRef = (ColumnReference)obj.getLeftExpression();
Table parentTable = columnRef.getTable().getMetadataObject();
Entity entity = getEntity(parentTable);
if (entity != null) {
Column column = columnRef.getMetadataObject();
if (parentTable.getPrimaryKey().getColumnByName(column.getName())!=null) {
entity.addKeyValue(column, (Literal)obj.getRightExpression());
return true;
}
}
}
if (obj.getLeftExpression() instanceof ColumnReference && obj.getRightExpression() instanceof ColumnReference) {
Column left = ((ColumnReference)obj.getLeftExpression()).getMetadataObject();
Column right = ((ColumnReference)obj.getRightExpression()).getMetadataObject();
if (isJoinOrPkColumn(left)&& isJoinOrPkColumn(right)) {
// in odata the navigation from parent to child implicit by their keys
Entity leftEntity = getEntity((Table)left.getParent());
Entity rightEntity = getEntity((Table)right.getParent());
leftEntity.addRelation(left, right, rightEntity);
rightEntity.addRelation(right,left, leftEntity);
return true;
}
}
}
return false;
}
private boolean isJoinOrPkColumn(Column column) {
boolean joinColumn = Boolean.valueOf(column.getProperty(ODataMetadataProcessor.JOIN_COLUMN, false));
if (!joinColumn) {
Table table = (Table)column.getParent();
return (table.getPrimaryKey().getColumnByName(column.getName()) != null);
}
return false;
}
private boolean valid() {
for (Entity e:this.entities) {
if (e.hasValidKey()) {
return true;
}
}
return false;
}
}
}