/*
* Copyright (C) 2011 Benoit GUEROUT <bguerout at gmail dot com> and Yves AMSELLEM <amsellem dot yves at gmail dot com>
*
* 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 org.jongo.query;
import com.mongodb.BasicDBList;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONCallback;
import org.bson.BSON;
import org.bson.BSONObject;
import org.jongo.bson.Bson;
import org.jongo.bson.BsonDocument;
import org.jongo.marshall.Marshaller;
import org.jongo.marshall.MarshallingException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class BsonQueryFactory implements QueryFactory {
private static final String DEFAULT_TOKEN = "#";
private static final String MARSHALL_OPERATOR = "$marshall";
private final String token;
private final Marshaller marshaller;
private static class BsonQuery implements Query {
private final DBObject dbo;
public BsonQuery(DBObject dbo) {
this.dbo = dbo;
}
public DBObject toDBObject() {
return dbo;
}
}
public BsonQueryFactory(Marshaller marshaller) {
this(marshaller, DEFAULT_TOKEN);
}
public BsonQueryFactory(Marshaller marshaller, String token) {
this.token = token;
this.marshaller = marshaller;
}
public Query createQuery(final String query, Object... parameters) {
if (query == null) {
return new BsonQuery((DBObject) JSON.parse(query));
}
if (parameters == null) {
parameters = new Object[]{null};
}
// We have two different cases:
//
// - tokens as property names "{scores.#: 1}": they must be expanded before going
// through the JSON parser, and their toString() is inserted in the query
//
// - tokens as property values "{id: #}": they are resolved by the JSON parser and
// therefore marshalled as DBObjects (actually LazyDBObjects).
StringBuilder sb = new StringBuilder();
int paramIncrement = 0; // how many params must be skipped by the next value param
int paramPos = 0; // current position in the parameter list
int start = 0; // start of the current string segment
int pos; // position of the last token found
while ((pos = query.indexOf(token, start)) != -1) {
if (paramPos >= parameters.length) {
throw new IllegalArgumentException("Not enough parameters passed to query: " + query);
}
// Insert chars before the token
sb.append(query, start, pos);
// Check if the character preceding the token is one that separates values.
// Otherwise, it's a property name substitution
if (isValueToken(query, pos)) {
// Will be resolved by the JSON parser below
sb.append("{\"").append(MARSHALL_OPERATOR).append("\":").append(paramIncrement).append("}");
paramIncrement = 0;
} else {
// Resolve it now
sb.append(parameters[paramPos]);
paramIncrement++;
}
paramPos++;
start = pos + token.length();
}
// Add remaining chars
sb.append(query, start, query.length());
if (paramPos < parameters.length) {
throw new IllegalArgumentException("Too many parameters passed to query: " + query);
}
final Object[] params = parameters;
// Parse the query with a callback that will weave in marshalled parameters
DBObject dbo;
try {
dbo = (DBObject) JSON.parse(sb.toString(), new JSONCallback() {
int paramPos = 0;
@Override
public Object objectDone() {
String name = curName();
Object o = super.objectDone();
if (o instanceof BSONObject && !(o instanceof List<?>)) {
BSONObject dbo = (BSONObject) o;
Object marshallValue = dbo.get(MARSHALL_OPERATOR);
if (marshallValue != null) {
paramPos += ((Number) marshallValue).intValue();
if (paramPos >= params.length) {
throw new IllegalArgumentException("Not enough parameters passed to query: " + query);
}
o = marshallParameter(params[paramPos++]);
// Replace value set by super.objectDone()
if (!isStackEmpty()) {
_put(name, o);
} else {
o = !BSON.hasDecodeHooks() ? o : BSON.applyDecodingHooks(o);
setRoot(o);
}
}
}
if (isStackEmpty()) {
// End of object
}
return o;
}
});
} catch (Exception e) {
throw new IllegalArgumentException("Cannot parse query: " + query, e);
}
return new BsonQuery(dbo);
}
private boolean isValueToken(String query, int tokenIndex) {
for (int pos = tokenIndex; pos >= 0; pos--) {
char c = query.charAt(pos);
if (c == ':') {
return true;
} else if (c == '{' || c == '.') {
return false;
} else if (c == ',') {
return !isPropertyName(query, pos-1);
}
}
return true;
}
private boolean isPropertyName(String query, int tokenIndex) {
for (int pos = tokenIndex; pos >= 0; pos--) {
char c = query.charAt(pos);
if (c == '[') {
return false;
} else if (c == '{') {
return true;
}
}
return false;
}
private Object marshallParameter(Object parameter) {
try {
if (parameter == null || Bson.isPrimitive(parameter)) {
return parameter;
}
if (parameter instanceof Collection) {
return marshallCollection((Collection<?>) parameter);
}
if (parameter instanceof Object[]) {
return marshallArray((Object[]) parameter);
}
return marshallDocument(parameter);
} catch (Exception e) {
String message = String.format("Unable to marshall parameter: %s", parameter);
throw new MarshallingException(message, e);
}
}
private DBObject marshallArray(Object[] parameters) {
BasicDBList list = new BasicDBList();
for (int i = 0; i < parameters.length; i++) {
list.add(marshallParameter(parameters[i]));
}
return list;
}
private DBObject marshallCollection(Collection<?> parameters) {
BasicDBList list = new BasicDBList();
for (Object param : parameters) {
list.add(marshallParameter(param));
}
return list;
}
private Object marshallDocument(Object parameter) {
if (parameter instanceof Enum) {
return marshallPrimitiveWithWrapper(parameter);
} else {
BsonDocument document = marshaller.marshall(parameter);
DBObject dbo = document.toDBObject();
if (dbo.keySet().isEmpty()) {
return marshallPrimitiveWithWrapper(parameter);
} else {
return dbo;
}
}
}
/**
* The object may have been serialized to a primitive type with a
* custom serializer, so try again after wrapping as an object property.
* We do this trick only as a fallback since it causes Jackson to consider the parameter
* as "Object" and thus ignore any annotations that may exist on its actual class.
*/
private Object marshallPrimitiveWithWrapper(Object parameter) {
final BsonDocument document;
Map<String, Object> primitiveWrapper = Collections.singletonMap("wrapped", parameter);
document = marshaller.marshall(primitiveWrapper);
return document.toDBObject().get("wrapped");
}
}