/*
* 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.mongodb;
import static org.teiid.language.visitor.SQLStringVisitor.getRecordName;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import org.bson.types.ObjectId;
import org.teiid.api.exception.query.FunctionExecutionException;
import org.teiid.core.types.ClobType;
import org.teiid.core.types.GeometryType;
import org.teiid.core.types.TransformationException;
import org.teiid.core.types.basic.ClobToStringTransform;
import org.teiid.language.*;
import org.teiid.language.Join.JoinType;
import org.teiid.language.SortSpecification.Ordering;
import org.teiid.language.visitor.HierarchyVisitor;
import org.teiid.metadata.*;
import org.teiid.query.function.GeometryUtils;
import org.teiid.translator.TranslatorException;
import org.teiid.translator.mongodb.MergeDetails.Association;
import com.mongodb.*;
public class MongoDBSelectVisitor extends HierarchyVisitor {
private AtomicInteger aliasCount = new AtomicInteger();
private AtomicInteger columnCount = new AtomicInteger();
protected MongoDBExecutionFactory executionFactory;
protected RuntimeMetadata metadata;
private Select command;
protected ArrayList<TranslatorException> exceptions = new ArrayList<TranslatorException>();
protected Stack<Object> onGoingExpression = new Stack<Object>();
protected ConcurrentHashMap<Object, ColumnDetail> expressionMap = new ConcurrentHashMap<Object, ColumnDetail>();
private HashMap<String, BasicDBObject> groupByProjections = new HashMap<String, BasicDBObject>();
protected MongoDocument mongoDoc;
// derived stuff
protected BasicDBObject project = new BasicDBObject();
protected Integer limit;
protected Integer skip;
protected DBObject sort;
protected DBObject match;
protected DBObject having;
protected BasicDBObject group = new BasicDBObject();
protected ArrayList<String> selectColumns = new ArrayList<String>();
protected ArrayList<String> selectColumnReferences = new ArrayList<String>();
protected boolean projectBeforeMatch = false;
protected MergePlanner mergePlanner = new MergePlanner();
protected ArrayList<Condition> pendingConditions = new ArrayList<Condition>();
protected LinkedList<MongoDocument> joinedDocuments = new LinkedList<MongoDocument>();
private boolean processingDerivedColumn = false;
public MongoDBSelectVisitor(MongoDBExecutionFactory executionFactory, RuntimeMetadata metadata) {
this.executionFactory = executionFactory;
this.metadata = metadata;
}
/**
* Appends the string form of the LanguageObject to the current buffer.
* @param obj the language object instance
*/
public void append(LanguageObject obj) {
if (obj != null) {
visitNode(obj);
}
}
/**
* Simple utility to append a list of language objects to the current buffer
* by creating a comma-separated list.
* @param items a list of LanguageObjects
*/
protected void append(List<? extends LanguageObject> items) {
if (items != null && items.size() != 0) {
append(items.get(0));
for (int i = 1; i < items.size(); i++) {
append(items.get(i));
}
}
}
/**
* Simple utility to append an array of language objects to the current buffer
* by creating a comma-separated list.
* @param items an array of LanguageObjects
*/
protected void append(LanguageObject[] items) {
if (items != null && items.length != 0) {
append(items[0]);
for (int i = 1; i < items.length; i++) {
append(items[i]);
}
}
}
public String getColumnName(ColumnReference obj) {
String elemShortName = null;
AbstractMetadataRecord elementID = obj.getMetadataObject();
if(elementID != null) {
elemShortName = getRecordName(elementID);
} else {
elemShortName = obj.getName();
}
return elemShortName;
}
@Override
public void visit(DerivedColumn obj) {
Expression teiidExpression = obj.getExpression();
String alias = getAlias(obj.getAlias());
this.processingDerivedColumn = true;
append(teiidExpression);
Object mongoExpression = this.onGoingExpression.pop();
ColumnDetail exprDetails = this.expressionMap.get(mongoExpression);
if (exprDetails == null) {
exprDetails = new ColumnDetail();
exprDetails.addProjectedName(alias);
this.expressionMap.put(mongoExpression, exprDetails);
}
// the the expression is already part of group by then the projection should be $_id.{name}
this.selectColumns.add(alias);
if (exprDetails.partOfGroupBy) {
BasicDBObject id = this.groupByProjections.get("_id"); //$NON-NLS-1$
this.project.append(alias, id.get(exprDetails.getProjectedName()));
exprDetails.addProjectedName(alias);
this.selectColumnReferences.add(alias);
}
else {
exprDetails.addProjectedName(alias);
exprDetails.partOfProject = true;
if (teiidExpression instanceof ColumnReference) {
String elementName = getColumnName((ColumnReference)obj.getExpression());
this.selectColumnReferences.add(elementName);
// the the expression is already part of group by then the projection should be $_id.{name}
if (this.command.isDistinct() || this.groupByProjections.get(alias) != null) {
// this is DISTINCT case
this.project.append(alias, "$_id."+alias); //$NON-NLS-1$
// if group by does not exist then build the group root id based on distinct
this.group.put(alias, mongoExpression);
}
else {
this.project.append(alias, mongoExpression);
}
}
else {
implicitProject(teiidExpression, mongoExpression, exprDetails, alias);
// what user sees as project
this.selectColumnReferences.add(alias);
}
}
this.processingDerivedColumn = false;
}
private String getAlias(String alias) {
if (alias == null) {
return "_m"+this.aliasCount.getAndIncrement(); //$NON-NLS-1$
}
return alias;
}
private ColumnDetail buildAlias() {
return buildAlias("_m"+this.aliasCount.getAndIncrement()); //$NON-NLS-1$
}
private ColumnDetail buildAlias(String alias) {
if (alias == null) {
return buildAlias();
}
ColumnDetail detail = new ColumnDetail();
detail.addProjectedName(alias);
return detail;
}
@Override
public void visit(ColumnReference obj) {
try {
if (obj.getMetadataObject() == null) {
for (Object expr:this.expressionMap.keySet()) {
ColumnDetail columnInfo = this.expressionMap.get(expr);
if (columnInfo.hasProjectedName(getColumnName(obj))) {
this.onGoingExpression.push(expr);
break;
}
}
}
else {
// do not allow array type in where clauses etc.
/*
if (!this.processingDerivedColumn) {
if (DataTypeManager.isArrayType(obj.getMetadataObject().getRuntimeType())){
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18027, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18027, getColumnName(obj))));
}
}
*/
ColumnDetail columnInfo = buildColumnDetail(obj);
Object mongoExpr = columnInfo.expression;
this.onGoingExpression.push(mongoExpr);
this.expressionMap.putIfAbsent(mongoExpr, columnInfo);
}
} catch (TranslatorException e) {
this.exceptions.add(e);
return;
}
}
ColumnDetail buildColumnDetail(ColumnReference obj) throws TranslatorException {
MongoDocument columnDocument = getDocument(obj.getTable().getMetadataObject());
MongoDocument targetDocument = this.mongoDoc.getTargetDocument();
String columnName = obj.getMetadataObject().getName();
String documentFieldName = obj.getMetadataObject().getName();
// column is on the same collection
if (columnDocument.equals(targetDocument)) {
documentFieldName = columnDocument.getColumnName(columnName);
}
else if (targetDocument.embeds(columnDocument)){
// if this is embeddable table, then we need to use the embedded collection name
MergeDetails ref = targetDocument.getEmbeddedDocumentReferenceKey(columnDocument);
String parentColumnName = ref.getParentColumnName(columnName);
if (parentColumnName != null) {
while(ref.isNested()) {
columnDocument = ref.getDocument();
ref = targetDocument.getEmbeddedDocumentReferenceKey(columnDocument);
parentColumnName = ref.getParentColumnName(parentColumnName);
}
documentFieldName = targetDocument.getColumnName(parentColumnName);
}
else {
documentFieldName = columnDocument.getDocumentName() + "." + columnDocument.getColumnName(columnName); //$NON-NLS-1$
}
}
else if (targetDocument.merges(columnDocument)){
documentFieldName = columnDocument.getColumnName(columnName);
}
ColumnDetail detail = new ColumnDetail();
detail.addProjectedName(documentFieldName);
detail.documentFieldName = documentFieldName;
detail.expression = "$"+documentFieldName; //$NON-NLS-1$
return detail;
}
private MongoDocument getDocument(Table table) {
if (this.mongoDoc != null && this.mongoDoc.getTable().getName().equals(table.getName())) {
return this.mongoDoc;
}
for (MongoDocument doc:this.joinedDocuments) {
if (doc.getTable().getName().equals(table.getName())) {
return doc;
}
}
return null;
}
@Override
public void visit(AggregateFunction obj) {
if (!obj.getParameters().isEmpty()) {
append(obj.getParameters());
}
BasicDBObject expr = null;
if (obj.getName().equals(AggregateFunction.COUNT)) {
// this is only true for count(*) case, so we need implicit group id clause
try {
Object param = this.onGoingExpression.pop();
BasicDBList eq = new BasicDBList();
eq.add(0, param);
eq.add(1, null);
BasicDBList values = new BasicDBList();
values.add(0, new BasicDBObject("$eq", eq)); //$NON-NLS-1$
values.add(1, 0);
values.add(2, 1);
expr = new BasicDBObject("$sum",new BasicDBObject("$cond", values)); //$NON-NLS-1$ //$NON-NLS-2$
} catch (EmptyStackException e) {
this.group.put("_id", null); //$NON-NLS-1$
expr = new BasicDBObject("$sum", new Integer(1)); //$NON-NLS-1$
}
}
else if (obj.getName().equals(AggregateFunction.AVG)) {
expr = new BasicDBObject("$avg", this.onGoingExpression.pop()); //$NON-NLS-1$
}
else if (obj.getName().equals(AggregateFunction.SUM)) {
expr = new BasicDBObject("$sum", this.onGoingExpression.pop()); //$NON-NLS-1$
}
else if (obj.getName().equals(AggregateFunction.MIN)) {
expr = new BasicDBObject("$min", this.onGoingExpression.pop()); //$NON-NLS-1$
}
else if (obj.getName().equals(AggregateFunction.MAX)) {
expr = new BasicDBObject("$max", this.onGoingExpression.pop()); //$NON-NLS-1$
}
else {
this.exceptions.add(new TranslatorException(MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18005, obj.getName())));
}
if (expr != null) {
this.onGoingExpression.push(expr);
}
}
private ColumnDetail addToProject(Object expr, boolean addExprAsProject, ColumnDetail detail, boolean needsProjection, String projectedName) {
if (detail == null) {
// if expression is in having/where clause there is will be no alias; however mongo expects some functions
// to be elevated to project before $match can be run
if (needsProjection) {
this.projectBeforeMatch = true;
}
detail = buildAlias();
this.expressionMap.putIfAbsent(expr, detail);
projectedName = detail.getProjectedName();
}
detail.expression = expr;
if (needsProjection) {
if (this.project.get(projectedName) == null && !this.project.values().contains(expr)) {
this.project.append(projectedName, addExprAsProject?expr:1);
}
detail.partOfProject = true;
}
else {
detail.partOfProject = false;
}
return detail;
}
@Override
public void visit(Function obj) {
String functionName = obj.getName();
if (functionName.indexOf('.') != -1) {
functionName = functionName.substring(functionName.indexOf('.')+1);
}
if (this.executionFactory.getFunctionModifiers().containsKey(functionName)) {
List<?> parts = this.executionFactory.getFunctionModifiers().get(functionName).translate(obj);
if (parts != null) {
obj = (Function)parts.get(0);
}
}
BasicDBObject expr = null;
if (isGeoSpatialFunction(functionName)) {
try {
expr = (BasicDBObject)handleGeoSpatialFunction(functionName, obj);
} catch (TranslatorException e) {
this.exceptions.add(e);
}
}
else if (isStringFunction(functionName)) {
expr = (BasicDBObject)handleStringFunction(functionName, obj);
}
else {
List<Expression> args = obj.getParameters();
if (args != null) {
BasicDBList params = new BasicDBList();
for (int i = 0; i < args.size(); i++) {
append(args.get(i));
Object param = this.onGoingExpression.pop();
params.add(param);
}
expr = new BasicDBObject(obj.getName(), params);
}
}
if(expr != null) {
this.onGoingExpression.push(expr);
}
}
private boolean isStringFunction(String functionName) {
if (functionName.equalsIgnoreCase("UCASE")
|| functionName.equalsIgnoreCase("LCASE")
|| functionName.equalsIgnoreCase("SUBSTRING")) {
return true;
}
return false;
}
private BasicDBObject handleStringFunction(String functionName, Function function) {
List<Expression> args = function.getParameters();
BasicDBObject func = null;
append(args.get(0));
Object column = this.onGoingExpression.pop();
if (args.size() == 1) {
func = new BasicDBObject(function.getName(), column);
}
else {
BasicDBList params = new BasicDBList();
params.add(column);
for (int i = 1; i < args.size(); i++) {
append(args.get(i));
Object param = this.onGoingExpression.pop();
params.add(param);
}
func = new BasicDBObject(function.getName(), params);
}
BasicDBObject ne = buildNE(column.toString(), null);
return buildCondition(ne, func, null);
}
private BasicDBObject buildCondition(Object expr, Object trueExpr, Object falseExpr) {
BasicDBList values = new BasicDBList();
values.add(0, expr);
values.add(1, trueExpr);
values.add(2, falseExpr);
return new BasicDBObject("$cond", values);
}
private BasicDBObject buildNE(Object leftExpr, Object rightExpr) {
BasicDBList values = new BasicDBList();
values.add(0, leftExpr);
values.add(1, rightExpr);
return new BasicDBObject("$ne", values);
}
private boolean isGeoSpatialFunction(String name) {
for (String func:MongoDBExecutionFactory.GEOSPATIAL_FUNCTIONS) {
if (name.equalsIgnoreCase(func)) {
return true;
}
}
return false;
}
@Override
public void visit(NamedTable obj) {
try {
this.mongoDoc = new MongoDocument(obj.getMetadataObject(), this.metadata);
configureUnwind(this.mongoDoc);
} catch (TranslatorException e) {
this.exceptions.add(e);
}
}
@Override
public void visit(Join obj) {
try {
if (obj.getLeftItem() instanceof Join) {
append(obj.getLeftItem());
Table right = ((NamedTable)obj.getRightItem()).getMetadataObject();
processJoin(this.mongoDoc, new MongoDocument(right, this.metadata), obj.getCondition(), obj.getJoinType());
}
else if (obj.getRightItem() instanceof Join) {
Table left = ((NamedTable)obj.getLeftItem()).getMetadataObject();
append(obj.getRightItem());
processJoin(this.mongoDoc, new MongoDocument(left, this.metadata), obj.getCondition(), obj.getJoinType());
}
else {
Table left = ((NamedTable)obj.getLeftItem()).getMetadataObject();
Table right = ((NamedTable)obj.getRightItem()).getMetadataObject();
processJoin(new MongoDocument(left, this.metadata), new MongoDocument(right, this.metadata), obj.getCondition(), obj.getJoinType());
}
} catch (TranslatorException e) {
this.exceptions.add(e);
}
}
private void configureUnwind(MongoDocument document) throws TranslatorException {
if (document.isMerged()) {
// if nested document
MongoDocument mergeDocument = document.getMergeDocument();
if (mergeDocument.isMerged()) {
configureUnwind(mergeDocument);
}
if (document.getMergeAssociation() == Association.MANY) {
this.mergePlanner.addNode(new UnwindNode(document));
}
else {
this.mergePlanner.addNode(new ExistsNode(document));
}
}
}
private void processJoin(MongoDocument left, MongoDocument right, Condition cond, JoinType joinType) throws TranslatorException {
// now adjust for the left/right outer depending upon who is the outer document
JoinCriteriaVisitor jcv = new JoinCriteriaVisitor(joinType, left, right, this.mergePlanner);
jcv.process(cond);
if (left.contains(right)) {
this.mongoDoc = left;
this.joinedDocuments.add(right);
configureUnwind(right);
}
else if (right.contains(left)) {
this.mongoDoc = right;
this.joinedDocuments.add(left);
configureUnwind(left);
}
else {
if (this.mongoDoc != null) {
// this is for nested grand kids
for (MongoDocument child:this.joinedDocuments) {
if (child.contains(right)) {
this.joinedDocuments.add(right);
configureUnwind(right);
return;
}
}
}
throw new TranslatorException(MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18012, left.getTable().getName(), right.getTable().getName()));
}
if (cond != null) {
this.pendingConditions.add(cond);
}
}
@Override
public void visit(Select obj) {
this.command = obj;
if (obj.getFrom() != null && !obj.getFrom().isEmpty()) {
append(obj.getFrom());
}
if (!this.exceptions.isEmpty()) {
return;
}
if (obj.getWhere() != null) {
append(obj.getWhere());
}
if (!this.onGoingExpression.isEmpty()) {
if (this.match != null) {
DBObject expr = (DBObject)this.onGoingExpression.pop();
ArrayList exprs = (ArrayList)expr.get("$and"); //$NON-NLS-1$
if (exprs != null) {
exprs.add(0, this.match);
this.match = expr;
}
else {
this.match = QueryBuilder.start().and(this.match, expr).get();
}
}
else {
this.match = (DBObject)this.onGoingExpression.pop();
}
}
else {
// default match in case no where clause used
// TEIID-2841 - in ONE-2-ONE case $unwind works as filter
}
if (obj.getGroupBy() != null) {
append(obj.getGroupBy());
}
append(obj.getDerivedColumns());
// in distinct since there may not be group by, but mongo requires a grouping clause.
if (obj.getGroupBy() == null && obj.isDistinct() && !this.group.containsField("_id")) { //$NON-NLS-1$
BasicDBObject id = new BasicDBObject(this.group);
this.group.clear();
this.group.put("_id", id); //$NON-NLS-1$
}
if (obj.getHaving() != null) {
append(obj.getHaving());
}
if (!this.onGoingExpression.isEmpty()) {
this.having = (DBObject)this.onGoingExpression.pop();
}
if (!this.group.isEmpty()) {
if (this.group.get("_id") == null) { //$NON-NLS-1$
this.group.put("_id", null); //$NON-NLS-1$
}
}
else {
this.group = null;
}
if (obj.getOrderBy() != null) {
append(obj.getOrderBy());
}
if (obj.getLimit() != null) {
append(obj.getLimit());
}
}
private ColumnDetail implicitProject(Expression teiidExpr, Object mongoExpr, ColumnDetail columnDetails, String projectedName) {
if (teiidExpr instanceof ColumnReference) {
return this.expressionMap.get(mongoExpr);
}
else if (teiidExpr instanceof AggregateFunction) {
ColumnDetail alias = addToProject(mongoExpr, false, columnDetails, true, projectedName);
if (!this.group.values().contains(mongoExpr)) {
this.group.put(projectedName, mongoExpr);
}
return alias;
}
else if (teiidExpr instanceof Function) {
Boolean avoidProjection = Boolean.valueOf(((Function) teiidExpr).getMetadataObject().getProperty(MongoDBExecutionFactory.AVOID_PROJECTION, false));
return addToProject(mongoExpr, true, columnDetails, processingDerivedColumn||!avoidProjection, projectedName);
}
else if (teiidExpr instanceof Condition) {
// needs to be in the form "_mo: {$cond: [{$eq :["$city", "FREEDOM"]}, true, false]}}}"
BasicDBList values = new BasicDBList();
values.add(0, mongoExpr);
values.add(1, true);
values.add(2, false);
return addToProject(new BasicDBObject("$cond", values), true, columnDetails, true, projectedName); //$NON-NLS-1$
}
else if (teiidExpr instanceof Literal) {
if (this.executionFactory.getVersion().compareTo(MongoDBExecutionFactory.TWO_6) >= 0) {
return addToProject(new BasicDBObject("$literal", mongoExpr), true, columnDetails, true, projectedName); //$NON-NLS-1$
}
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18026, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18026)));
}
return null;
}
@Override
public void visit(Comparison obj) {
// this for $cond in the select statement, and formatting of command for $cond vs $match is different
if (this.processingDerivedColumn) {
visitDerivedExpression(obj);
return;
}
// this for the normal where clause
ColumnDetail leftExprDetails = getExpressionAlias(obj.getLeftExpression());
append(obj.getRightExpression());
Object rightExpr = this.onGoingExpression.pop();
if (this.expressionMap.get(rightExpr) != null) {
rightExpr = this.expressionMap.get(rightExpr).getProjectedName();
}
QueryBuilder query = leftExprDetails.getQueryBuilder();
rightExpr = checkAndConvertToObjectId(obj.getLeftExpression(), obj.getRightExpression(), rightExpr);
buildComparisionQuery(obj, rightExpr, query);
if (leftExprDetails.partOfProject || obj.getLeftExpression() instanceof ColumnReference) {
this.onGoingExpression.push(query.get());
}
else {
this.onGoingExpression.push(buildFunctionQuery(obj, (BasicDBObject)leftExprDetails.expression, rightExpr));
}
if (obj.getLeftExpression() instanceof ColumnReference) {
ColumnReference column = (ColumnReference)obj.getLeftExpression();
this.mongoDoc.updateReferenceColumnValue(column.getTable().getName(), column.getName(), rightExpr);
}
}
private Object checkAndConvertToObjectId(Expression left, Expression right, Object rightValue) {
if (left instanceof ColumnReference && right instanceof Literal) {
String navtiveType = ((ColumnReference)left).getMetadataObject().getNativeType();
if (navtiveType != null && navtiveType.equals(ObjectId.class.getName())) {
return new ObjectId((String)rightValue);
}
}
return rightValue;
}
protected BasicDBObject buildFunctionQuery(Comparison obj, BasicDBObject leftExpr, Object rightExpr) {
switch(obj.getOperator()) {
case EQ:
if (rightExpr instanceof Boolean && ((Boolean)rightExpr)) {
return leftExpr;
}
//$FALL-THROUGH$
case NE:
case LT:
case LE:
case GT:
case GE:
}
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18030, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18030)));
return null;
}
protected void buildComparisionQuery(Comparison obj, Object rightExpr, QueryBuilder query) {
switch(obj.getOperator()) {
case EQ:
query.is(rightExpr);
break;
case NE:
query.notEquals(rightExpr);
break;
case LT:
query.lessThan(rightExpr);
break;
case LE:
query.lessThanEquals(rightExpr);
break;
case GT:
query.greaterThan(rightExpr);
break;
case GE:
query.greaterThanEquals(rightExpr);
break;
}
}
private void visitDerivedExpression(Comparison obj) {
append(obj.getLeftExpression());
Object leftExpr = this.onGoingExpression.pop();
append(obj.getRightExpression());
Object rightExpr = this.onGoingExpression.pop();
BasicDBList values = new BasicDBList();
values.add(0, leftExpr);
values.add(1, rightExpr);
switch(obj.getOperator()) {
case EQ:
this.onGoingExpression.push(new BasicDBObject("$eq", values)); //$NON-NLS-1$
break;
case NE:
this.onGoingExpression.push(new BasicDBObject("$ne", values)); //$NON-NLS-1$
break;
case LT:
this.onGoingExpression.push(new BasicDBObject("$lt", values)); //$NON-NLS-1$
break;
case LE:
this.onGoingExpression.push(new BasicDBObject("$lte", values)); //$NON-NLS-1$
break;
case GT:
this.onGoingExpression.push(new BasicDBObject("$gt", values)); //$NON-NLS-1$
break;
case GE:
this.onGoingExpression.push(new BasicDBObject("$gte", values)); //$NON-NLS-1$
break;
}
}
@Override
public void visit(AndOr obj) {
append(obj.getLeftCondition());
append(obj.getRightCondition());
DBObject right = (DBObject)this.onGoingExpression.pop();
DBObject left = (DBObject) this.onGoingExpression.pop();
switch(obj.getOperator()) {
case AND:
this.onGoingExpression.push(QueryBuilder.start().and(left, right).get());
break;
case OR:
this.onGoingExpression.push(QueryBuilder.start().or(left, right).get());
break;
}
}
@Override
public void visit(Array array) {
append(array.getExpressions());
BasicDBList values = new BasicDBList();
for (int i = 0; i < array.getExpressions().size(); i++) {
values.add(0, this.onGoingExpression.pop());
}
this.onGoingExpression.push(values);
}
@Override
public void visit(Literal obj) {
try {
this.onGoingExpression.push(this.executionFactory.convertToMongoType(obj.getValue(), null, null));
} catch (TranslatorException e) {
this.exceptions.add(e);
}
}
@Override
public void visit(In obj) {
append(obj.getLeftExpression());
Object expr = this.onGoingExpression.pop();
ColumnDetail detail = this.expressionMap.get(expr);
QueryBuilder query = QueryBuilder.start();
if (detail == null) {
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18031, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18031)));
}
else {
query = detail.getQueryBuilder();
this.onGoingExpression.push(buildInQuery(obj, query).get());
}
}
protected QueryBuilder buildInQuery(In obj, QueryBuilder query) {
append(obj.getRightExpressions());
BasicDBList values = new BasicDBList();
for (int i = 0; i < obj.getRightExpressions().size(); i++) {
Object rightExpr = this.onGoingExpression.pop();
rightExpr = checkAndConvertToObjectId(obj.getLeftExpression(), obj.getRightExpressions().get(i), rightExpr);
values.add(0, rightExpr);
}
if (obj.isNegated()) {
query.notIn(values);
} else {
query.in(values);
}
return query;
}
ColumnDetail getExpressionAlias(Expression obj) {
// the way DBRef names handled in projection vs selection is different.
// in projection we want to see as "col" mapped to "col.$_id" as it is treated as sub-document
// where as in selection it will should be "col._id".
append(obj);
Object expr = this.onGoingExpression.pop();
ColumnDetail detail = this.expressionMap.get(expr);
detail = implicitProject(obj, expr, detail, detail != null ? detail.getProjectedName():null);
// when expression shows up in a condition, but it is not a derived column
// then add implicit project on that alias.
return detail;
}
@Override
public void visit(IsNull obj) {
append(obj.getExpression());
Object expr = this.onGoingExpression.pop();
ColumnDetail detail = this.expressionMap.get(expr);
QueryBuilder query = QueryBuilder.start();
if (detail == null) {
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18032, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18032)));
}
else {
query = detail.getQueryBuilder();
this.onGoingExpression.push(buildIsNullQuery(obj, query).get());
}
}
protected QueryBuilder buildIsNullQuery(IsNull obj, QueryBuilder query) {
if (obj.isNegated()) {
query.notEquals(null);
}
else {
query.is(null);
}
return query;
}
@Override
public void visit(Like obj) {
append(obj.getLeftExpression());
Object expr = this.onGoingExpression.pop();
ColumnDetail detail = this.expressionMap.get(expr);
QueryBuilder query = QueryBuilder.start();
if (detail == null) {
this.exceptions.add(new TranslatorException(MongoDBPlugin.Event.TEIID18033, MongoDBPlugin.Util.gs(MongoDBPlugin.Event.TEIID18033)));
}
else {
query = detail.getQueryBuilder();
buildLikeQuery(obj, query);
this.onGoingExpression.push(query.get());
}
}
protected QueryBuilder buildLikeQuery(Like obj, QueryBuilder query) {
if (obj.isNegated()) {
query.not();
}
append(obj.getRightExpression());
StringBuilder value = new StringBuilder((String)this.onGoingExpression.pop());
int idx = -1;
while (true) {
idx = value.indexOf("%", idx+1);//$NON-NLS-1$
if (idx != -1 && idx == 0) {
continue;
}
if (idx != -1 && idx == value.length()-1) {
continue;
}
if (idx == -1) {
break;
}
value.replace(idx, idx+1, ".*"); //$NON-NLS-1$
}
if (value.charAt(0) != '%') {
value.insert(0, '^');
}
idx = value.length();
if (value.charAt(idx-1) != '%') {
value.insert(idx, '$');
}
String regex = value.toString().replaceAll("%", ""); //$NON-NLS-1$ //$NON-NLS-2$
query.is(Pattern.compile(regex));
return query;
}
@Override
public void visit(Limit obj) {
if (obj.getRowLimit() != Integer.MAX_VALUE) {
this.limit = new Integer(obj.getRowLimit());
}
this.skip = new Integer(obj.getRowOffset());
}
@Override
public void visit(OrderBy obj) {
append(obj.getSortSpecifications());
}
@Override
public void visit(SortSpecification obj) {
append(obj.getExpression());
Object expr = this.onGoingExpression.pop();
ColumnDetail alias = this.expressionMap.get(expr);
if (this.sort == null) {
this.sort = new BasicDBObject(alias.getProjectedName(), (obj.getOrdering() == Ordering.ASC)?1:-1);
}
else {
this.sort.put(alias.getProjectedName(), (obj.getOrdering() == Ordering.ASC)?1:-1);
}
}
@Override
public void visit(GroupBy obj) {
// since grouping requires additional step, this is done at a different pipeline stage.
// so, that requires additional in-direction.
if (obj.getElements().size() == 1) {
append(obj.getElements().get(0));
Object mongoExpr = this.onGoingExpression.pop();
ColumnDetail exprDetails = this.expressionMap.get(mongoExpr);
String projectedName = "_c"+this.columnCount.getAndIncrement(); //$NON-NLS-1$
exprDetails.addProjectedName(projectedName);
this.group.put("_id", new BasicDBObject(projectedName, mongoExpr)); //$NON-NLS-1$
this.groupByProjections.put("_id", new BasicDBObject(projectedName, "$_id."+projectedName)); //$NON-NLS-1$ //$NON-NLS-2$
exprDetails.partOfGroupBy = true;
}
else {
BasicDBObject fields = new BasicDBObject();
BasicDBObject exprs = new BasicDBObject();
for (Expression expr : obj.getElements()) {
append(expr);
Object mongoExpr = this.onGoingExpression.pop();
ColumnDetail exprDetails = this.expressionMap.get(mongoExpr);
String projectedName = "_c"+this.columnCount.getAndIncrement(); //$NON-NLS-1$
exprDetails.addProjectedName(projectedName);
exprs.put(projectedName, mongoExpr);
fields.put(projectedName, "$_id."+projectedName); //$NON-NLS-1$
exprDetails.partOfGroupBy = true;
}
this.group.put("_id", exprs); //$NON-NLS-1$
this.groupByProjections.put("_id", fields); //$NON-NLS-1$
}
}
static boolean isPartOfPrimaryKey(Table table, String columnName) {
KeyRecord pk = table.getPrimaryKey();
if (pk != null) {
for (Column column:pk.getColumns()) {
if (getRecordName(column).equals(columnName)) {
return true;
}
}
}
return false;
}
boolean hasCompositePrimaryKey(Table table) {
KeyRecord pk = table.getPrimaryKey();
return pk.getColumns().size() > 1;
}
static boolean isPartOfForeignKey(Table table, String columnName) {
for (ForeignKey fk : table.getForeignKeys()) {
for (Column column : fk.getColumns()) {
if (column.getName().equals(columnName)) {
return true;
}
}
}
return false;
}
static String getForeignKeyRefTable(Table table, String columnName) {
for (ForeignKey fk : table.getForeignKeys()) {
for (Column column : fk.getColumns()) {
if (column.getName().equals(columnName)) {
return fk.getReferenceTableName();
}
}
}
return null;
}
static List<String> getColumnNames(List<Column> columns){
ArrayList<String> names = new ArrayList<String>();
for (Column c:columns) {
names.add(c.getName());
}
return names;
}
static enum SpatialType {Point, LineString, Polygon, MultiPoint, MultiLineString};
private DBObject handleGeoSpatialFunction(String functionName, Function function) throws TranslatorException{
if (functionName.equalsIgnoreCase(MongoDBExecutionFactory.FUNC_GEO_NEAR) ||
functionName.equalsIgnoreCase(MongoDBExecutionFactory.FUNC_GEO_NEAR_SPHERE)) {
return buildGeoNearFunction(function);
}
return buildGeoFunction(function);
}
private DBObject buildGeoNearFunction(Function function) throws TranslatorException {
List<Expression> args = function.getParameters();
// Column Name
int paramIndex = 0;
ColumnDetail column = getExpressionAlias(args.get(paramIndex++));
BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
builder.push(column.documentFieldName);
builder.push(function.getName());
append(args.get(paramIndex++));
Object object = this.onGoingExpression.pop();
if (object instanceof GeometryType) {
convertGeometryToJson(builder, (GeometryType)object);
} else {
builder.push("$geometry");//$NON-NLS-1$
builder.add("type", SpatialType.Point.name());//$NON-NLS-1$
// walk the co-ordinates
BasicDBList coordinates = new BasicDBList();
coordinates.add(object);
builder.add("coordinates", coordinates); //$NON-NLS-1$
}
// maxdistance
append(args.get(paramIndex++));
BasicDBObjectBuilder b= builder.pop();
b.add("$maxDistance", this.onGoingExpression.pop()); //$NON-NLS-1$
if (this.executionFactory.getVersion().compareTo(MongoDBExecutionFactory.TWO_6) >= 0) {
// mindistance
append(args.get(paramIndex++));
b.add("$minDistance", this.onGoingExpression.pop()); //$NON-NLS-1$
}
return builder.get();
}
private DBObject buildGeoFunction(Function function) throws TranslatorException{
List<Expression> args = function.getParameters();
// Column Name
int paramIndex = 0;
ColumnDetail column = getExpressionAlias(args.get(paramIndex++));
BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
builder.push(column.documentFieldName);
builder.push(function.getName());
append(args.get(paramIndex++));
Object object = this.onGoingExpression.pop();
if (object instanceof GeometryType) {
convertGeometryToJson(builder, (GeometryType)object);
} else {
// Type: Point, LineString, Polygon..
SpatialType type = SpatialType.valueOf((String)object);
append(args.get(paramIndex++));
builder.push("$geometry");//$NON-NLS-1$
builder.add("type", type.name());//$NON-NLS-1$
// walk the co-ordinates
BasicDBList coordinates = new BasicDBList();
coordinates.add(this.onGoingExpression.pop());
builder.add("coordinates", coordinates); //$NON-NLS-1$
}
return builder.get();
}
private void convertGeometryToJson(BasicDBObjectBuilder builder, GeometryType object) throws TranslatorException {
try {
ClobType clob = GeometryUtils.geometryToGeoJson(object);
ClobToStringTransform clob2str = new ClobToStringTransform();
String geometry = (String)clob2str.transform(clob, String.class);
builder.add("$geometry", geometry);
} catch (FunctionExecutionException | TransformationException e) {
throw new TranslatorException(e);
}
}
}