/*******************************************************************************
* * Copyright 2012 Impetus Infotech.
* *
* * 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.impetus.client.mongodb.query;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.StringTokenizer;
import javax.persistence.Query;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.impetus.client.mongodb.MongoDBClient;
import com.impetus.client.mongodb.MongoEntityReader;
import com.impetus.client.mongodb.query.gis.GeospatialQueryFactory;
import com.impetus.client.mongodb.utils.MongoDBUtils;
import com.impetus.kundera.client.Client;
import com.impetus.kundera.client.ClientBase;
import com.impetus.kundera.client.EnhanceEntity;
import com.impetus.kundera.gis.geometry.Point;
import com.impetus.kundera.gis.query.GeospatialQuery;
import com.impetus.kundera.metadata.MetadataUtils;
import com.impetus.kundera.metadata.model.ApplicationMetadata;
import com.impetus.kundera.metadata.model.EntityMetadata;
import com.impetus.kundera.metadata.model.MetamodelImpl;
import com.impetus.kundera.metadata.model.attributes.AbstractAttribute;
import com.impetus.kundera.metadata.model.type.AbstractManagedType;
import com.impetus.kundera.persistence.EntityManagerFactoryImpl.KunderaMetadata;
import com.impetus.kundera.persistence.EntityReader;
import com.impetus.kundera.persistence.PersistenceDelegator;
import com.impetus.kundera.property.PropertyAccessorFactory;
import com.impetus.kundera.query.JPQLParseException;
import com.impetus.kundera.query.KunderaQuery;
import com.impetus.kundera.query.KunderaQuery.FilterClause;
import com.impetus.kundera.query.KunderaQuery.SortOrder;
import com.impetus.kundera.query.KunderaQuery.SortOrdering;
import com.impetus.kundera.query.KunderaQuery.UpdateClause;
import com.impetus.kundera.query.QueryHandlerException;
import com.impetus.kundera.query.QueryImpl;
import com.mongodb.BasicDBObject;
/**
* Query class for MongoDB data store.
*
* @author amresh.singh
*/
public class MongoDBQuery extends QueryImpl
{
/** The log used by this class. */
private static Logger log = LoggerFactory.getLogger(MongoDBQuery.class);
private boolean isSingleResult;
/**
* Instantiates a new mongo db query.
*
* @param jpaQuery
* the jpa query
* @param kunderaQuery
* the kundera query
* @param persistenceDelegator
* the persistence delegator
* @param persistenceUnits
* the persistence units
*/
public MongoDBQuery(KunderaQuery kunderaQuery, PersistenceDelegator persistenceDelegator,
final KunderaMetadata kunderaMetadata)
{
super(kunderaQuery, persistenceDelegator, kunderaMetadata);
}
/*
* (non-Javadoc)
*
* @see com.impetus.kundera.query.QueryImpl#executeUpdate()
*/
@Override
public int executeUpdate()
{
return super.executeUpdate();
}
/*
* (non-Javadoc)
*
* @see com.impetus.kundera.query.QueryImpl#setFirstResult(int)
*/
@Override
public Query setFirstResult(int firstResult)
{
return super.setFirstResult(firstResult);
}
/*
* (non-Javadoc)
*
* @see com.impetus.kundera.query.QueryImpl#setMaxResults(int)
*/
@Override
public Query setMaxResults(int maxResult)
{
return super.setMaxResults(maxResult);
}
/*
* (non-Javadoc)
*
* @see
* com.impetus.kundera.query.QueryImpl#populateEntities(com.impetus.kundera
* .metadata.model.EntityMetadata, com.impetus.kundera.client.Client)
*/
@Override
protected List<Object> populateEntities(EntityMetadata m, Client client)
{
ApplicationMetadata appMetadata = kunderaMetadata.getApplicationMetadata();
List<Object> result = new ArrayList<Object>();
try
{
String query = appMetadata.getQuery(getJPAQuery());
boolean isNative = kunderaQuery.isNative();
if (isNative)
{
throw new UnsupportedOperationException("Native query support is not enabled in mongoDB");
}
if (MetadataUtils.useSecondryIndex(((ClientBase) client).getClientMetadata()))
{
BasicDBObject orderByClause = getOrderByClause(m);
return ((MongoDBClient) client).loadData(m,
createMongoQuery(m, getKunderaQuery().getFilterClauseQueue()), null, orderByClause,
isSingleResult ? 1 : maxResult, firstResult, getKeys(m, getKunderaQuery().getResult()),
getKunderaQuery().getResult());
}
else
{
return populateUsingLucene(m, client, result, null);
}
}
catch (Exception e)
{
log.error("Error during executing query, Caused by:", e);
throw new QueryHandlerException(e);
}
}
@Override
protected List findUsingLucene(EntityMetadata m, Client client)
{
try
{
BasicDBObject orderByClause = getOrderByClause(m);
return ((MongoDBClient) client).loadData(m, createMongoQuery(m, getKunderaQuery().getFilterClauseQueue()),
null, orderByClause, isSingleResult ? 1 : maxResult, firstResult,
getKeys(m, getKunderaQuery().getResult()), getKunderaQuery().getResult());
}
catch (Exception e)
{
log.error("Error during executing query, Caused by:", e);
e.printStackTrace();
throw new QueryHandlerException(e);
}
}
@Override
protected List<Object> recursivelyPopulateEntities(EntityMetadata m, Client client)
{
// TODO : required to modify client return relation.
// if it is a parent..then find data related to it only
// else u need to load for associated fields too.
List<EnhanceEntity> ls = new ArrayList<EnhanceEntity>();
ApplicationMetadata appMetadata = kunderaMetadata.getApplicationMetadata();
try
{
String query = appMetadata.getQuery(getJPAQuery());
boolean isNative = kunderaQuery.isNative();
if (isNative)
{
throw new UnsupportedOperationException("Native query support is not enabled in mongoDB");
}
BasicDBObject orderByClause = getOrderByClause(m);
ls = ((MongoDBClient) client).loadData(m, createMongoQuery(m, getKunderaQuery().getFilterClauseQueue()),
m.getRelationNames(), orderByClause, isSingleResult ? 1 : maxResult, firstResult,
getKeys(m, getKunderaQuery().getResult()), getKunderaQuery().getResult());
}
catch (Exception e)
{
log.error("Error during executing query, Caused by:", e);
throw new QueryHandlerException(e);
}
return setRelationEntities(ls, client, m);
}
/*
* (non-Javadoc)
*
* @see com.impetus.kundera.query.QueryImpl#getReader()
*/
@Override
protected EntityReader getReader()
{
return new MongoEntityReader(kunderaQuery, kunderaMetadata);
}
static class QueryComponent
{
boolean isAnd;
Queue clauses = new LinkedList();
List<QueryComponent> children = new ArrayList<MongoDBQuery.QueryComponent>();
QueryComponent parent;
BasicDBObject actualQuery;
}
private void populateQueryComponents(EntityMetadata m, QueryComponent sq)
{
boolean hasChildren = false;
if (sq.children != null && sq.children.size() > 0)
{
hasChildren = true;
for (QueryComponent subQ : sq.children)
{
populateQueryComponents(m, subQ);
}
}
if (sq.clauses.size() > 0 || hasChildren)
{
if (sq.clauses.size() > 0)
sq.actualQuery = createSubMongoQuery(m, sq.clauses);
if (hasChildren)
{
List<BasicDBObject> childQs = new ArrayList<BasicDBObject>();
if (sq.clauses.size() > 0)
childQs.add(sq.actualQuery);
for (QueryComponent subQ : sq.children)
{
childQs.add(subQ.actualQuery);
}
if (sq.isAnd)
{
BasicDBObject dbo = new BasicDBObject("$and", childQs);
sq.actualQuery = dbo;
}
else
{
BasicDBObject dbo = new BasicDBObject("$or", childQs);
sq.actualQuery = dbo;
}
}
}
return;
}
private static QueryComponent getQueryComponent(Queue filterClauseQueue)
{
QueryComponent subQuery = new QueryComponent();
QueryComponent currentSubQuery = subQuery;
for (Object object : filterClauseQueue)
{
if (object instanceof FilterClause)
{
currentSubQuery.clauses.add(object);
}
else if (object instanceof String)
{
String interClauseConstruct = (String) object;
if (interClauseConstruct.equals("("))
{
QueryComponent temp = new QueryComponent();
currentSubQuery.children.add(temp);
temp.parent = currentSubQuery;
currentSubQuery = temp;
}
else if (interClauseConstruct.equals(")"))
{
currentSubQuery = currentSubQuery.parent;
}
else if (interClauseConstruct.equalsIgnoreCase("AND"))
{
currentSubQuery.isAnd = true;
}
else if (interClauseConstruct.equalsIgnoreCase("OR"))
{
currentSubQuery.isAnd = false;
}
}
}
return subQuery;
}
public BasicDBObject createMongoQuery(EntityMetadata m, Queue filterClauseQueue)
{
QueryComponent sq = getQueryComponent(filterClauseQueue);
populateQueryComponents(m, sq);
return sq.actualQuery == null ? new BasicDBObject() : sq.actualQuery;
}
/**
* Creates MongoDB Query object from filterClauseQueue.
*
* @param m
* the m
* @param filterClauseQueue
* the filter clause queue
* @param columns
* @return the basic db object
*/
public BasicDBObject createSubMongoQuery(EntityMetadata m, Queue filterClauseQueue)
{
BasicDBObject query = new BasicDBObject();
BasicDBObject compositeColumns = new BasicDBObject();
MetamodelImpl metaModel = (MetamodelImpl) kunderaMetadata.getApplicationMetadata().getMetamodel(
m.getPersistenceUnit());
for (Object object : filterClauseQueue)
{
boolean isCompositeColumn = false;
boolean isSubCondition = false;
if (object instanceof FilterClause)
{
FilterClause filter = (FilterClause) object;
String property = filter.getProperty();
String condition = filter.getCondition();
Object value = filter.getValue().get(0);
// value is string but field.getType is different, then get
// value using
Field f = null;
// if alias is still present .. means it is an enclosing
// document search.
if (((AbstractAttribute) m.getIdAttribute()).getJPAColumnName().equalsIgnoreCase(property))
{
property = "_id";
f = (Field) m.getIdAttribute().getJavaMember();
if (metaModel.isEmbeddable(m.getIdAttribute().getBindableJavaType())
&& value.getClass().isAssignableFrom(f.getType()))
{
EmbeddableType compoundKey = metaModel.embeddable(m.getIdAttribute().getBindableJavaType());
compositeColumns = MongoDBUtils.getCompoundKeyColumns(m, value, compoundKey);
isCompositeColumn = true;
continue;
}
}
else if (metaModel.isEmbeddable(m.getIdAttribute().getBindableJavaType())
&& StringUtils.contains(property, '.'))
{
// Means it is a case of composite column.
property = property.substring(property.indexOf(".") + 1);
isCompositeColumn = true;
} /*
* if a composite key. "." assuming "." is part of property in
* case of embeddable only
*/
else if (StringUtils.contains(property, '.'))
{
EntityType entity = metaModel.entity(m.getEntityClazz());
StringTokenizer tokenizer = new StringTokenizer(property, ".");
String embeddedAttributeAsStr = tokenizer.nextToken();
String embeddableAttributeAsStr = tokenizer.nextToken();
Attribute embeddedAttribute = entity.getAttribute(embeddedAttributeAsStr);
EmbeddableType embeddableEntity = metaModel.embeddable(((AbstractAttribute) embeddedAttribute)
.getBindableJavaType());
f = (Field) embeddableEntity.getAttribute(embeddableAttributeAsStr).getJavaMember();
property = ((AbstractAttribute) embeddedAttribute).getJPAColumnName()
+ "."
+ ((AbstractAttribute) embeddableEntity.getAttribute(embeddableAttributeAsStr))
.getJPAColumnName();
}
else
{
EntityType entity = metaModel.entity(m.getEntityClazz());
String discriminatorColumn = ((AbstractManagedType) entity).getDiscriminatorColumn();
if (!property.equals(discriminatorColumn))
{
String fieldName = m.getFieldName(property);
f = (Field) entity.getAttribute(fieldName).getJavaMember();
}
}
if (value.getClass().isAssignableFrom(String.class) && f != null
&& !f.getType().equals(value.getClass()))
{
value = PropertyAccessorFactory.getPropertyAccessor(f).fromString(f.getType().getClass(),
value.toString());
}
value = MongoDBUtils.populateValue(value, value.getClass());
// Property, if doesn't exist in entity, may be there in a
// document embedded within it, so we have to check that
// TODO: Query should actually be in a format
// documentName.embeddedDocumentName.column, remove below if
// block once this is decided
// Query could be geospatial in nature
if (f != null && f.getType().equals(Point.class))
{
GeospatialQuery geospatialQueryimpl = GeospatialQueryFactory.getGeospatialQueryImplementor(
condition, value);
query = (BasicDBObject) geospatialQueryimpl.createGeospatialQuery(property, value, query);
}
else
{
if (isCompositeColumn)
{
property = new StringBuffer("_id.").append(property).toString();
}
if (condition.equals("="))
{
query.append(property, value);
}
else if (condition.equalsIgnoreCase("like"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$regex",
createLikeRegex((String) value)));
}
else
{
query.append(property, new BasicDBObject("$regex", createLikeRegex((String) value)));
}
}
else if (condition.equalsIgnoreCase(">"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$gt", value));
}
else
{
query.append(property, new BasicDBObject("$gt", value));
}
}
else if (condition.equalsIgnoreCase(">="))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$gte", value));
}
else
{
query.append(property, new BasicDBObject("$gte", value));
}
}
else if (condition.equalsIgnoreCase("<"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$lt", value));
}
else
{
query.append(property, new BasicDBObject("$lt", value));
}
}
else if (condition.equalsIgnoreCase("<="))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$lte", value));
}
else
{
query.append(property, new BasicDBObject("$lte", value));
}
}
else if (condition.equalsIgnoreCase("in"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$in", filter.getValue()));
}
else
{
query.append(property, new BasicDBObject("$in", filter.getValue()));
}
}
else if (condition.equalsIgnoreCase("not in"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$nin", filter.getValue()));
}
else
{
query.append(property, new BasicDBObject("$nin", filter.getValue()));
}
}
else if (condition.equalsIgnoreCase("<>"))
{
if (query.containsField(property))
{
query.get(property);
query.put(property, ((BasicDBObject) query.get(property)).append("$ne", value));
}
else
{
query.append(property, new BasicDBObject("$ne", value));
}
}
}
// TODO: Add support for other operators like >, <, >=, <=,
// order by asc/ desc, limit, skip, count etc
}
}
if (!compositeColumns.isEmpty())
{
query.append("_id", compositeColumns);
}
return query;
}
private BasicDBObject getKeys(EntityMetadata m, String[] columns)
{
BasicDBObject keys = new BasicDBObject();
if (columns != null && columns.length > 0)
{
MetamodelImpl metaModel = (MetamodelImpl) kunderaMetadata.getApplicationMetadata().getMetamodel(
m.getPersistenceUnit());
EntityType entity = metaModel.entity(m.getEntityClazz());
for (int i = 1; i < columns.length; i++)
{
if (columns[i] != null)
{
Attribute col = entity.getAttribute(columns[i]);
if (col == null)
{
throw new QueryHandlerException("column type is null for: " + columns);
}
keys.put(((AbstractAttribute) col).getJPAColumnName(), 1);
}
}
}
return keys;
}
/**
* Prepare order by clause.
*
* @return order by clause.
*/
private BasicDBObject getOrderByClause(final EntityMetadata metadata)
{
BasicDBObject orderByClause = null;
Metamodel metaModel = kunderaMetadata.getApplicationMetadata().getMetamodel(metadata.getPersistenceUnit());
EntityType entityType = metaModel.entity(metadata.getEntityClazz());
List<SortOrdering> orders = kunderaQuery.getOrdering();
if (orders != null)
{
orderByClause = new BasicDBObject();
for (SortOrdering order : orders)
{
orderByClause.append(getColumnName(metadata, entityType, order.getColumnName()), order.getOrder()
.equals(SortOrder.ASC) ? 1 : -1);
}
}
return orderByClause;
}
/*
* (non-Javadoc)
*
* @see com.impetus.kundera.query.QueryImpl#onExecuteUpdate()
*/
@Override
protected int onExecuteUpdate()
{
int ret = handleSpecialFunctions();
if (ret == -1)
{
return onUpdateDeleteEvent();
}
return ret;
}
/** The Constant SINGLE_STRING_KEYWORDS. */
public static final String[] FUNCTION_KEYWORDS = { "INCREMENT\\(\\d+\\)", "DECREMENT\\(\\d+\\)" };
private int handleSpecialFunctions()
{
boolean needsSpecialAttention = false;
outer: for (UpdateClause c : kunderaQuery.getUpdateClauseQueue())
{
for (int i = 0; i < FUNCTION_KEYWORDS.length; i++)
{
if (c.getValue() instanceof String)
{
String func = c.getValue().toString();
func = func.replaceAll(" ", "");
if (func.toUpperCase().matches(FUNCTION_KEYWORDS[i]))
{
needsSpecialAttention = true;
c.setValue(func);
break outer;
}
}
}
}
if (!needsSpecialAttention)
return -1;
EntityMetadata m = getEntityMetadata();
Metamodel metaModel = kunderaMetadata.getApplicationMetadata().getMetamodel(m.getPersistenceUnit());
Queue filterClauseQueue = kunderaQuery.getFilterClauseQueue();
BasicDBObject query = createMongoQuery(m, filterClauseQueue);
BasicDBObject update = new BasicDBObject();
for (UpdateClause c : kunderaQuery.getUpdateClauseQueue())
{
String columName = getColumnName(m, metaModel.entity(m.getEntityClazz()), c.getProperty());
boolean isSpecialFunction = false;
for (int i = 0; i < FUNCTION_KEYWORDS.length; i++)
{
if (c.getValue() instanceof String
&& c.getValue().toString().toUpperCase().matches(FUNCTION_KEYWORDS[i]))
{
isSpecialFunction = true;
if (c.getValue().toString().toUpperCase().startsWith("INCREMENT("))
{
String val = c.getValue().toString().toUpperCase();
val = val.substring(10, val.indexOf(")"));
update.put("$inc", new BasicDBObject(columName, Integer.valueOf(val)));
}
else if (c.getValue().toString().toUpperCase().startsWith("DECREMENT("))
{
String val = c.getValue().toString().toUpperCase();
val = val.substring(10, val.indexOf(")"));
update.put("$inc", new BasicDBObject(columName, -Integer.valueOf(val)));
}
}
}
if (!isSpecialFunction)
{
update.put(columName, c.getValue());
}
}
Client client = persistenceDelegeator.getClient(m);
return ((MongoDBClient) client).handleUpdateFunctions(query, update, m.getTableName());
}
@Override
public void close()
{
// TODO Auto-generated method stub
}
@Override
public Iterator iterate()
{
EntityMetadata m = getEntityMetadata();
Client client = persistenceDelegeator.getClient(m);
return new ResultIterator((MongoDBClient) client, m, createMongoQuery(m, getKunderaQuery()
.getFilterClauseQueue()), getOrderByClause(m), getKeys(m, getKunderaQuery().getResult()),
persistenceDelegeator, getFetchSize() != null ? getFetchSize() : this.maxResult);
}
private String getColumnName(EntityMetadata metadata, EntityType entityType, String property)
{
String columnName = null;
if (property.indexOf(".") > 0)
{
property = property.substring((kunderaQuery.getEntityAlias() + ".").length());
}
try
{
columnName = ((AbstractAttribute) entityType.getAttribute(property)).getJPAColumnName();
}
catch (IllegalArgumentException iaex)
{
log.warn("No column found by this name : " + property + " checking for embeddedfield");
}
// where condition may be for search within embedded object
if (columnName == null && property.indexOf(".") > 0)
{
String enclosingEmbeddedField = MetadataUtils.getEnclosingEmbeddedFieldName(metadata, property, true,
kunderaMetadata);
if (enclosingEmbeddedField != null)
{
columnName = property;
}
}
if (columnName == null)
{
log.error("No column found by this name : " + property);
throw new JPQLParseException("No column found by this name : " + property + ". Check your query.");
}
return columnName;
}
/**
* Create regular expression equivalent to any like operator string match
* function
*
* @param expr
* @return
*/
public static String createLikeRegex(String expr)
{
String regex = createRegex(expr);
regex = regex.replace("_", ".").replace("%", ".*?");
return regex;
}
/**
* Generates the regular expression for matching string for like operator
*
* @param value
* @return
*/
public static String createRegex(String value)
{
if (value == null)
{
throw new IllegalArgumentException("String cannot be null");
}
int len = value.length();
if (len == 0)
{
return "";
}
StringBuilder sb = new StringBuilder(len * 2);
sb.append("(?i)^");
for (int i = 0; i < len; i++)
{
char c = value.charAt(i);
if ("[](){}.*+?$^|#\\".indexOf(c) != -1)
{
sb.append("\\");
}
sb.append(c);
}
sb.append("$");
return sb.toString();
}
}