/*
* Hibernate OGM, Domain model persistence for NoSQL datastores
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.ogm.datastore.mongodb.query.parsing.nativequery.impl;
import org.hibernate.ogm.datastore.mongodb.query.impl.MongoDBQueryDescriptor.Operation;
import org.parboiled.BaseParser;
import org.parboiled.Rule;
import org.parboiled.annotations.BuildParseTree;
import org.parboiled.annotations.SuppressNode;
import org.parboiled.annotations.SuppressSubnodes;
import com.mongodb.util.JSON;
/**
* A parser for MongoDB queries which can be given in one of the following representations:
* <ul>
* <li>Criteria-only find query, e.g. <code>{ $and: [ { name : 'Portia' }, { author : 'Oscar Wilde' } ] }</code>. It is
* left to MongoDB's own {@link JSON} parser to interpret such queries.
* <li>As "invocation" of the MongoDB shell API (CLI), e.g.
* <code>db.WILDE_POEM.find({ '$query' : { 'name' : 'Athanasia' }, '$orderby' : { 'name' : 1 } })</code>. Currently the
* following API methods are supported:
* <ul>
* <li>find(criteria)</li>
* <li>find(criteria, projection)</li>
* <li>findOne(criteria)</li>
* <li>findOne(criteria, projection)</li>
* <li>findAndModify(document)</li>
* <li>insert(document or array)</li>
* <li>insert(document or array, options)</li>
* <li>remove(criteria)</li>
* <li>remove(criteria, options)</li>
* <li>update(criteria, update)</li>
* <li>update(criteria, update, options)</li>
* <li>count()</li>
* <li>count(criteria)</li>
* <li>aggregate(criteria)</li>
* </ul>
* The parameter values must be given as JSON objects adhering to the <a
* href="http://docs.mongodb.org/manual/reference/mongodb-extended-json/">strict mode</a> of MongoDB's JSON handling,
* with the relaxation that Strings may not only be given in double quotes but also single quotes to facilitate their
* specification in Java Strings.</li>
* </ul>
*
* @author Davide D'Alto
* @author Gunnar Morling
* @author Thorsten Möller
* @author Guillaume Smet
*/
@BuildParseTree
public class NativeQueryParser extends BaseParser<MongoDBQueryDescriptorBuilder> {
final MongoDBQueryDescriptorBuilder builder;
public NativeQueryParser() {
this.builder = new MongoDBQueryDescriptorBuilder();
}
public Rule Query() {
return Sequence( FirstOf( ParsedQuery(), CriteriaOnlyFindQuery() ), EOI, push( builder ) );
}
public Rule ParsedQuery() {
return Sequence( Db(), Collection(), Operation() );
}
/**
* A find query only given as criterion. Leave it to MongoDB's own parser to handle it.
*
* @return the {@link Rule} to identify a find query only
*/
public Rule CriteriaOnlyFindQuery() {
return Sequence( ZeroOrMore( ANY ), builder.setOperation( Operation.FIND ), builder.setCriteria( match() ) );
}
@SuppressNode
public Rule Db() {
return Sequence( ZeroOrMore( WhiteSpace() ), "db ", Separator() );
}
@SuppressSubnodes
public Rule Collection() {
return Sequence( OneOrMore( TestNot( Reserved() ), ANY ), builder.setCollection( match() ) );
//TODO OGM-949 it should not be just ANY matcher as they are some restrictions in the Collection naming in Mongo
// cf. https://docs.mongodb.org/manual/faq/developers/#are-there-any-restrictions-on-the-names-of-collections
}
@SuppressNode
public Rule Separator() {
return Sequence( ZeroOrMore( WhiteSpace() ), ". " );
}
public Rule Reserved() {
return FirstOf( Find(), FindOne(), FindAndModify(), Insert(), InsertOne(), InsertMany(), Remove(), Update(), Count(), Aggregate(), Distinct() );
// TODO There are many more query types than what we support.
}
public Rule Operation() {
return FirstOf(
Sequence( Find(), builder.setOperation( Operation.FIND ) ),
Sequence( FindOne(), builder.setOperation( Operation.FINDONE ) ),
Sequence( FindAndModify(), builder.setOperation( Operation.FINDANDMODIFY ) ),
Sequence( Insert(), builder.setOperation( Operation.INSERT ) ),
Sequence( InsertOne(), builder.setOperation( Operation.INSERTONE ) ),
Sequence( InsertMany(), builder.setOperation( Operation.INSERTMANY ) ),
Sequence( Remove(), builder.setOperation( Operation.REMOVE ) ),
Sequence( Update(), builder.setOperation( Operation.UPDATE ) ),
Sequence( Count(), builder.setOperation( Operation.COUNT ) ),
Sequence( Aggregate(), builder.setOperation( Operation.AGGREGATE_PIPELINE ) ),
Sequence( Distinct(), builder.setOperation( Operation.DISTINCT ) )
);
}
public Rule Find() {
return Sequence(
Separator(),
"find ",
"( ",
JsonObject(), builder.setCriteria( match() ),
Optional( Sequence( ", ", JsonObject(), builder.setProjection( match() ) ) ),
") "
);
}
public Rule FindOne() {
return Sequence(
Separator(),
"findOne ",
"( ",
Optional( JsonObject(), builder.setCriteria( match() ) ),
Optional( Sequence( ", ", JsonObject(), builder.setProjection( match() ) ) ),
") "
);
}
public Rule FindAndModify() {
return Sequence(
Separator(),
"findAndModify ",
"( ",
JsonObject(), builder.setCriteria( match() ),
") "
);
}
public Rule Insert() {
return Sequence(
Separator(),
"insert ",
"( ",
JsonComposite(), builder.setUpdateOrInsert( match() ),
Optional( Sequence( ", ", JsonObject(), builder.setOptions( match() ) ) ),
") "
);
}
public Rule InsertOne() {
return Sequence(
Separator(),
"insertOne ",
"( ",
JsonComposite(), builder.setUpdateOrInsert( match() ),
Optional( Sequence( ", ", JsonObject(), builder.setOptions( match() ) ) ),
") "
);
}
public Rule InsertMany() {
return Sequence(
Separator(),
"insertMany ",
"( ",
JsonComposite(), builder.setUpdateOrInsert( match() ),
Optional( Sequence( ", ", JsonObject(), builder.setOptions( match() ) ) ),
") "
);
}
public Rule Remove() {
return Sequence(
Separator(),
"remove ",
"( ",
JsonObject(), builder.setCriteria( match() ),
Optional( Sequence( ", ",
FirstOf(
Sequence( BooleanValue(), builder.setOptions( "{ 'justOne': " + match() + " }" ) ),
Sequence( JsonObject(), builder.setOptions( match() ) )
)
) ),
") "
);
}
public Rule Update() {
return Sequence(
Separator(),
"update ",
"( ",
JsonObject(), builder.setCriteria( match() ), ", ",
JsonObject(), builder.setUpdateOrInsert( match() ),
Optional( Sequence( ", ", JsonObject(), builder.setOptions( match() ) ) ),
") "
);
}
public Rule Aggregate() {
return Sequence( Separator(), "aggregate ", "( ", AggregateArray(), ") " );
}
public Rule AggregateArray() {
return Sequence(
"[ ",
Sequence(
AggregateObject(),
ZeroOrMore( Sequence( ", ", AggregateObject() ) ) ),
"] " );
}
public Rule AggregateObject() {
return Sequence(
ZeroOrMore( WhiteSpace() ).skipNode(),
"{ ", AggregatePair(), "} " );
}
public Rule AggregatePair() {
return Sequence(
JsonString(), builder.push( currentIndex(), match() ),
": ",
Value(), builder.addPipeline( builder.pop(), match() ) );
}
public Rule Count() {
return Sequence(
Separator(),
"count ",
"( ",
Optional( Sequence( JsonComposite(), builder.setCriteria( match() ) ) ),
") "
);
}
public Rule Distinct() {
return Sequence(
Separator(),
"distinct ",
"( ",
Sequence( JsonString(), builder.setDistinctFieldName( JSON.parse( match() ).toString() ) ),
Optional( Sequence( ", ", JsonObject(), builder.setCriteria( match() ) ) ),
Optional( Sequence( ", ", JsonObject(), builder.setCollation( match() ) ) ),
") "
);
}
public Rule JsonComposite() {
return FirstOf( JsonObject(), JsonArray() );
}
public Rule JsonObject() {
return Sequence(
ZeroOrMore( WhiteSpace() ).skipNode(),
"{ ",
FirstOf(
Sequence( Pair(), ZeroOrMore( Sequence( ", ", Pair() ) ) ),
Optional( Pair() )
).suppressNode(),
"} "
);
}
public Rule Pair() {
return Sequence( JsonString(), ": ", Value() );
}
public Rule Value() {
return FirstOf( PrimitiveValue(), JsonComposite(), BsonFunctionCall() );
}
public Rule PrimitiveValue() {
return FirstOf( JsonString(), JsonNumber(), "true ", "false ", "null ",
"Infinity ", "NaN ", "undefined " );
}
public Rule BooleanValue() {
return FirstOf( "true", "false" );
}
@SuppressNode
public Rule JsonNumber() {
return Sequence( Integer(), Optional( Sequence( Frac(), Optional( Exp() ) ) ), ZeroOrMore( WhiteSpace() ) );
}
public Rule JsonArray() {
return Sequence(
"[ ",
FirstOf(
Sequence( Value(), ZeroOrMore( Sequence( ", ", Value() ) ) ),
Optional( Value() )
),
"] "
);
}
@SuppressSubnodes
public Rule JsonString() {
return FirstOf( JsonDoubleQuotedString(), JsonSingleQuotedString() );
}
@SuppressSubnodes
public Rule JsonDoubleQuotedString() {
return Sequence( "\"", ZeroOrMore( Character() ), "\" " );
}
@SuppressSubnodes
public Rule JsonSingleQuotedString() {
return Sequence( "'", ZeroOrMore( SingleQuotedStringCharacter() ), "' " );
}
@SuppressSubnodes
public Rule BsonFunctionCall() {
return Sequence( Optional( "new " ), SupportedBsonFunction(), ZeroOrMore( WhiteSpace() ), "( ",
FirstOf(
Sequence( PrimitiveValue(), ZeroOrMore( Sequence( ", ", PrimitiveValue() ) ) ),
Optional( PrimitiveValue() )
)
, ") " );
}
public Rule SupportedBsonFunction() {
return FirstOf( "BinData", "Date", "HexData", "ISODate", "NumberInt", "NumberLong", "ObjectId", "Timestamp", "RegExp", "DBPointer",
"UUID", "GUID", "CSUUID", "CSGUID", "JUUID", "JGUID", "PYUUID", "PYGUID" );
}
public Rule Character() {
return FirstOf( EscapedChar(), NormalChar() );
}
public Rule SingleQuotedStringCharacter() {
return FirstOf( SingleQuotedStringEscapedChar(), SingleQuotedStringNormalChar() );
}
public Rule EscapedChar() {
return Sequence( "\\", FirstOf( AnyOf( "\"\\/bfnrt" ), Unicode() ) );
}
public Rule SingleQuotedStringEscapedChar() {
return Sequence( "\\", FirstOf( AnyOf( "'\\/bfnrt" ), Unicode() ) );
}
public Rule NormalChar() {
return Sequence( TestNot( AnyOf( "\"\\" ) ), ANY );
}
public Rule SingleQuotedStringNormalChar() {
return Sequence( TestNot( AnyOf( "'\\" ) ), ANY );
}
public Rule Unicode() {
return Sequence( "u", HexDigit(), HexDigit(), HexDigit(), HexDigit() );
}
public Rule Integer() {
return Sequence( Optional( "-" ), NonZeroDigit(), ZeroOrMore( Digit() ) );
}
public Rule Digits() {
return OneOrMore( Digit() );
}
public Rule Digit() {
return CharRange( '0', '9' );
}
public Rule NonZeroDigit() {
return CharRange( '1', '9' );
}
public Rule HexDigit() {
return FirstOf( CharRange( '0', '9' ), CharRange( 'a', 'f' ), CharRange( 'A', 'F' ) );
}
public Rule Frac() {
return Sequence( ".", Digits() );
}
public Rule Exp() {
return Sequence( IgnoreCase( "e" ), Optional( AnyOf( "+-" ) ), Digits() );
}
@SuppressNode
public Rule WhiteSpace() {
return OneOrMore( AnyOf( " \n\r\t\f" ) );
}
@Override
@SuppressNode
protected Rule fromStringLiteral(final String string) {
if ( string.endsWith( " " ) ) {
return Sequence( string.trim(), Optional( WhiteSpace() ) );
}
else {
return String( string );
}
}
}