/*
* Copyright 2011, Mysema Ltd
*
* 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 com.mysema.query.mongodb;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.bson.BSONObject;
import org.bson.types.ObjectId;
import com.google.common.collect.Sets;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mysema.query.types.Constant;
import com.mysema.query.types.Expression;
import com.mysema.query.types.ExpressionUtils;
import com.mysema.query.types.FactoryExpression;
import com.mysema.query.types.Operation;
import com.mysema.query.types.OperationImpl;
import com.mysema.query.types.Operator;
import com.mysema.query.types.Ops;
import com.mysema.query.types.Order;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.ParamExpression;
import com.mysema.query.types.Path;
import com.mysema.query.types.PathMetadata;
import com.mysema.query.types.PathType;
import com.mysema.query.types.SubQueryExpression;
import com.mysema.query.types.TemplateExpression;
import com.mysema.query.types.Visitor;
/**
* Serializes the given Querydsl query to a DBObject query for MongoDB
*
* @author laimw
*
*/
public abstract class MongodbSerializer implements Visitor<Object, Void> {
public Object handle(Expression<?> expression) {
return expression.accept(this, null);
}
public DBObject toSort(List<OrderSpecifier<?>> orderBys) {
BasicDBObject sort = new BasicDBObject();
for (OrderSpecifier<?> orderBy : orderBys) {
Object key = orderBy.getTarget().accept(this, null);
sort.append(key.toString(), orderBy.getOrder() == Order.ASC ? 1 : -1);
}
return sort;
}
@Override
public Object visit(Constant<?> expr, Void context) {
if (Enum.class.isAssignableFrom(expr.getType())) {
return ((Enum<?>)expr.getConstant()).name();
} else {
return expr.getConstant();
}
}
@Override
public Object visit(TemplateExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
@Override
public Object visit(FactoryExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
private String asDBKey(Operation<?> expr, int index) {
return (String) asDBValue(expr, index);
}
private Object asDBValue(Operation<?> expr, int index) {
return expr.getArg(index).accept(this, null);
}
private String regexValue(Operation<?> expr, int index) {
return Pattern.quote(expr.getArg(index).accept(this, null).toString());
}
protected DBObject asDBObject(String key, Object value) {
return new BasicDBObject(key, value);
}
@Override
public Object visit(Operation<?> expr, Void context) {
Operator<?> op = expr.getOperator();
if (op == Ops.EQ) {
if (expr.getArg(0) instanceof Operation) {
Operation<?> lhs = (Operation<?>) expr.getArg(0);
if (lhs.getOperator() == Ops.COL_SIZE || lhs.getOperator() == Ops.ARRAY_SIZE) {
return asDBObject(asDBKey(lhs, 0), asDBObject("$size", asDBValue(expr, 1)));
} else {
throw new UnsupportedOperationException("Illegal operation " + expr);
}
} else if (isReference(expr, 0)) {
return asDBObject(asDBKey(expr, 0), asReference(expr, 1));
} else {
return asDBObject(asDBKey(expr, 0), asDBValue(expr, 1));
}
} else if (op == Ops.STRING_IS_EMPTY) {
return asDBObject(asDBKey(expr, 0), "");
} else if (op == Ops.AND) {
BSONObject lhs = (BSONObject) handle(expr.getArg(0));
BSONObject rhs = (BSONObject) handle(expr.getArg(1));
if (Sets.intersection(lhs.keySet(), rhs.keySet()).isEmpty()) {
lhs.putAll(rhs);
return lhs;
} else {
BasicDBList list = new BasicDBList();
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
return asDBObject("$and", list);
}
} else if (op == Ops.NOT) {
//Handle the not's child
BasicDBObject arg = (BasicDBObject) handle(expr.getArg(0));
//Only support the first key, let's see if there
//is cases where this will get broken
String key = arg.keySet().iterator().next();
Operation<?> subOperation = (Operation<?>) expr.getArg(0);
Operator<?> subOp = subOperation.getOperator();
if (subOp == Ops.IN) {
return visit(OperationImpl.create(Boolean.class, Ops.NOT_IN, subOperation.getArg(0),
subOperation.getArg(1)), context);
} else if (subOp != Ops.EQ && subOp != Ops.STRING_IS_EMPTY) {
return asDBObject(key, asDBObject("$not", arg.get(key)));
} else {
return asDBObject(key, asDBObject("$ne", arg.get(key)));
}
} else if (op == Ops.OR) {
BasicDBList list = new BasicDBList();
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
return asDBObject("$or", list);
} else if (op == Ops.NE) {
if (isReference(expr, 0)) {
return asDBObject(asDBKey(expr, 0), asDBObject("$ne", asReference(expr, 1)));
} else {
return asDBObject(asDBKey(expr, 0), asDBObject("$ne", asDBValue(expr, 1)));
}
} else if (op == Ops.STARTS_WITH) {
return asDBObject(asDBKey(expr, 0),
Pattern.compile("^" + regexValue(expr, 1)));
} else if (op == Ops.STARTS_WITH_IC) {
return asDBObject(asDBKey(expr, 0),
Pattern.compile("^" + regexValue(expr, 1), Pattern.CASE_INSENSITIVE));
} else if (op == Ops.ENDS_WITH) {
return asDBObject(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$"));
} else if (op == Ops.ENDS_WITH_IC) {
return asDBObject(asDBKey(expr, 0),
Pattern.compile(regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE));
} else if (op == Ops.EQ_IGNORE_CASE) {
return asDBObject(asDBKey(expr, 0),
Pattern.compile("^" + regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE));
} else if (op == Ops.STRING_CONTAINS) {
return asDBObject(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*"));
} else if (op == Ops.STRING_CONTAINS_IC) {
return asDBObject(asDBKey(expr, 0),
Pattern.compile(".*" + regexValue(expr, 1) + ".*", Pattern.CASE_INSENSITIVE));
} else if (op == Ops.MATCHES) {
return asDBObject(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString()));
} else if (op == Ops.MATCHES_IC) {
return asDBObject(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString(), Pattern.CASE_INSENSITIVE));
} else if (op == Ops.LIKE) {
String regex = ExpressionUtils.likeToRegex((Expression)expr.getArg(1)).toString();
return asDBObject(asDBKey(expr, 0), Pattern.compile(regex));
} else if (op == Ops.BETWEEN) {
BasicDBObject value = new BasicDBObject("$gte", asDBValue(expr, 1));
value.append("$lte", asDBValue(expr, 2));
return asDBObject(asDBKey(expr, 0), value);
} else if (op == Ops.IN) {
int constIndex = 0;
int exprIndex = 1;
if (expr.getArg(1) instanceof Constant<?>) {
constIndex = 1;
exprIndex = 0;
}
if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) {
Collection<?> values = (Collection<?>) ((Constant<?>) expr.getArg(constIndex)).getConstant();
return asDBObject(asDBKey(expr, exprIndex), asDBObject("$in", values.toArray()));
} else {
if (isReference(expr, exprIndex)) {
return asDBObject(asDBKey(expr, exprIndex), asReference(expr, constIndex));
} else {
return asDBObject(asDBKey(expr, exprIndex), asDBValue(expr, constIndex));
}
}
} else if (op == Ops.NOT_IN) {
int constIndex = 0;
int exprIndex = 1;
if (expr.getArg(1) instanceof Constant<?>) {
constIndex = 1;
exprIndex = 0;
}
if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) {
Collection<?> values = (Collection<?>) ((Constant<?>) expr.getArg(constIndex)).getConstant();
return asDBObject(asDBKey(expr, exprIndex), asDBObject("$nin", values.toArray()));
} else {
if (isReference(expr, exprIndex)) {
return asDBObject(asDBKey(expr, exprIndex),
asDBObject("$ne", asReference(expr, constIndex)));
} else {
return asDBObject(asDBKey(expr, exprIndex),
asDBObject("$ne", asDBValue(expr, constIndex)));
}
}
} else if (op == Ops.COL_IS_EMPTY) {
BasicDBList list = new BasicDBList();
list.add(asDBObject(asDBKey(expr, 0), new BasicDBList()));
list.add(asDBObject(asDBKey(expr, 0), asDBObject("$exists", false)));
return asDBObject("$or", list);
} else if (op == Ops.LT) {
return asDBObject(asDBKey(expr, 0), asDBObject("$lt", asDBValue(expr, 1)));
} else if (op == Ops.GT) {
return asDBObject(asDBKey(expr, 0), asDBObject("$gt", asDBValue(expr, 1)));
} else if (op == Ops.LOE) {
return asDBObject(asDBKey(expr, 0), asDBObject("$lte", asDBValue(expr, 1)));
} else if (op == Ops.GOE) {
return asDBObject(asDBKey(expr, 0), asDBObject("$gte", asDBValue(expr, 1)));
} else if (op == Ops.IS_NULL) {
return asDBObject(asDBKey(expr, 0), asDBObject("$exists", false));
} else if (op == Ops.IS_NOT_NULL) {
return asDBObject(asDBKey(expr, 0), asDBObject("$exists", true));
} else if (op == Ops.CONTAINS_KEY) {
Path<?> path = (Path<?>) expr.getArg(0);
Expression<?> key = expr.getArg(1);
return asDBObject(visit(path, context) + "." + key.toString(), asDBObject("$exists", true));
} else if (op == MongodbOps.NEAR) {
return asDBObject(asDBKey(expr, 0), asDBObject("$near", asDBValue(expr, 1)));
} else if (op == MongodbOps.ELEM_MATCH) {
return asDBObject(asDBKey(expr, 0), asDBObject("$elemMatch", asDBValue(expr, 1)));
}
throw new UnsupportedOperationException("Illegal operation " + expr);
}
protected DBRef asReference(Operation<?> expr, int constIndex) {
return asReference(((Constant<?>)expr.getArg(constIndex)).getConstant());
}
protected DBRef asReference(Object constant) {
// override in subclass
throw new UnsupportedOperationException();
}
protected boolean isReference(Operation<?> expr, int exprIndex) {
Expression<?> arg = expr.getArg(exprIndex);
if (arg instanceof Path) {
return isReference((Path<?>) arg);
} else {
return false;
}
}
protected boolean isReference(Path<?> arg) {
// override in subclass
return false;
}
@Override
public String visit(Path<?> expr, Void context) {
PathMetadata<?> metadata = expr.getMetadata();
if (metadata.getParent() != null) {
if (metadata.getPathType() == PathType.COLLECTION_ANY) {
return visit(metadata.getParent(), context);
} else if (metadata.getParent().getMetadata().getPathType() != PathType.VARIABLE) {
String rv = getKeyForPath(expr, metadata);
return visit(metadata.getParent(), context) + "." + rv;
}
}
return getKeyForPath(expr, metadata);
}
protected String getKeyForPath(Path<?> expr, PathMetadata<?> metadata) {
if (expr.getType().equals(ObjectId.class)) {
return "_id";
} else {
return metadata.getElement().toString();
}
}
@Override
public Object visit(SubQueryExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
@Override
public Object visit(ParamExpression<?> expr, Void context) {
throw new UnsupportedOperationException();
}
}