/* * 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 java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; import org.hibernate.ogm.datastore.mongodb.query.impl.MongoDBQueryDescriptor; import org.hibernate.ogm.datastore.mongodb.query.impl.MongoDBQueryDescriptor.Operation; import org.hibernate.ogm.util.impl.StringHelper; import com.mongodb.client.model.Collation; import com.mongodb.client.model.CollationAlternate; import com.mongodb.client.model.CollationCaseFirst; import com.mongodb.client.model.CollationMaxVariable; import com.mongodb.client.model.CollationStrength; import org.bson.Document; /** * Builder for {@link MongoDBQueryDescriptor}s. * * @author Gunnar Morling * @author Thorsten Möller * @author Guillaume Smet */ public class MongoDBQueryDescriptorBuilder { private String collection; private Operation operation; /** * Overloaded to be the 'document' for a FINDANDMODIFY query (which is a kind of criteria), */ private String criteria; private String projection; private String orderBy; /** * Distinct Operation will be performed on this field */ private String distinctFieldName; /** * Collation document */ private String collation; /** * Document or array of documents to insert/update for an INSERT/UPDATE query. */ private String updateOrInsert; private String options; private Set<Integer> parsed = new HashSet<>(); private List<Document> pipeline = new ArrayList<>(); private Deque<StackedOperation> stack = new ArrayDeque<>(); public static class PipelineOperation { private final String command; private final String value; public PipelineOperation(String command, String value) { this.command = command; this.value = value; } public String getCommand() { return command; } public String getValue() { return value; } } private static class StackedOperation { private final int index; private final String operation; public StackedOperation(int index, String operation) { this.index = index; this.operation = operation; } public int getIndex() { return index; } public String getOperation() { return operation; } } public boolean setCollection(String collection) { this.collection = collection.trim(); return true; } public boolean setOperation(Operation operation) { this.operation = operation; return true; } public boolean setCriteria(String criteria) { this.criteria = criteria; return true; } public boolean setProjection(String projection) { this.projection = projection; return true; } public boolean setOrderBy(String orderBy) { this.orderBy = orderBy; return true; } public boolean setOptions(String options) { this.options = options; return true; } public boolean setUpdateOrInsert(String updateOrInsert) { this.updateOrInsert = updateOrInsert; return true; } public boolean setDistinctFieldName(String fieldName) { this.distinctFieldName = fieldName.trim(); return true; } public boolean setCollation(String collation) { this.collation = collation; return true; } public MongoDBQueryDescriptor build() { //@todo redactor the spagetti! if ( operation != Operation.AGGREGATE_PIPELINE ) { MongoDBQueryDescriptor descriptor = null; if ( operation == Operation.DISTINCT ) { descriptor = new MongoDBQueryDescriptor( collection, operation, parse( criteria ), parseCollation( collation ), distinctFieldName ); } else if ( operation == Operation.INSERTMANY ) { // must be array Object anyDocs = parseAsObject( updateOrInsert ); List<Document> documents = (List<Document>) parseAsObject( updateOrInsert ); descriptor = new MongoDBQueryDescriptor( collection, operation, parse( criteria ), parse( projection ), parse( orderBy ), parse( options ), null, documents, null ); } else if ( operation == Operation.INSERT ) { //can be document or array Object anyDocs = parseAsObject( updateOrInsert ); if ( anyDocs instanceof List ) { //this is array descriptor = new MongoDBQueryDescriptor( collection, operation, parse( criteria ), parse( projection ), parse( orderBy ), parse( options ), null, (List<Document>) anyDocs, null ); } else { //this is one document descriptor = new MongoDBQueryDescriptor( collection, operation, parse( criteria ), parse( projection ), parse( orderBy ), parse( options ), (Document) anyDocs, null, null ); } } else { descriptor = new MongoDBQueryDescriptor( collection, operation, parse( criteria ), parse( projection ), parse( orderBy ), parse( options ), parse( updateOrInsert ), null, null ); } return descriptor; } return new MongoDBQueryDescriptor( collection, operation, pipeline ); } /** * Currently, there is no way to parse an array while supporting BSON and JSON extended syntax. So for now, we build * an object from the JSON string representing an array or an object, parse this object then extract the array/object. * * See <a href="https://jira.mongodb.org/browse/JAVA-2186">https://jira.mongodb.org/browse/JAVA-2186</a>. * * @param json a JSON string representing an array or an object * @return returns the array ({@code List}) (for many documents) or the object ({@code Document}) for one document * @see <a href="https://docs.mongodb.com/manual/tutorial/insert-documents/">insert documents</a> */ private Document parse(String json) { return (Document) parseAsObject( json ); } /** * parse JSON * @param json * @return * @see <a href="http://stackoverflow.com/questions/34436952/json-parse-equivalent-in-mongo-driver-3-x-for-java"> JSON.parse equivalent</a> */ private static Object parseAsObject(String json) { if ( StringHelper.isNullOrEmptyString( json ) ) { return null; } Document object = Document.parse( "{ 'json': " + json + "}" ); return object.get( "json" ); } private static Collation parseCollation(String json) { Document dbObject = ( (Document) parseAsObject( json ) ); if ( dbObject != null ) { dbObject = (Document) dbObject.get( "collation" ); if ( dbObject != null ) { Collation collation = Collation.builder() .locale( (String) dbObject.get( "locale" ) ) .caseLevel( (Boolean) dbObject.get( "caseLevel" ) ) .numericOrdering( (Boolean) dbObject.get( "numericOrdering" ) ) .backwards( (Boolean) dbObject.get( "backwards" ) ) .collationCaseFirst( caseFirst( dbObject ) ) .collationStrength( strength( dbObject ) ) .collationAlternate( alternate( dbObject ) ) .collationMaxVariable( maxVariable( dbObject ) ) .build(); return collation; } } return null; } private static CollationCaseFirst caseFirst(Document dbObject) { String caseFirst = dbObject.getString( "caseFirst" ); return caseFirst == null ? null : CollationCaseFirst.fromString( caseFirst ); } private static CollationStrength strength(Document dbObject) { Integer strength = dbObject.getInteger( "strength" ); return strength == null ? null : CollationStrength.fromInt( strength ); } private static CollationAlternate alternate(Document dbObject) { String value = dbObject.getString( "alternate" ); return value == null ? null : CollationAlternate.fromString( value ); } private static CollationMaxVariable maxVariable(Document dbObject) { String value = dbObject.getString( "maxVariable" ); return value == null ? null : CollationMaxVariable.fromString( value ); } private static Document operation(StackedOperation operation, String value) { Document stage = new Document(); stage.put( normalize( operation ), parseAsObject( value ) ); return stage; } public boolean addPipeline(StackedOperation operation, String value) { if ( !parsed.contains( operation.getIndex() ) ) { parsed.add( operation.getIndex() ); pipeline.add( operation( operation, value ) ); } return true; } private static String normalize(StackedOperation operation) { return operation.getOperation().replaceAll( "'", "" ).replaceAll( "\"", "" ).trim(); } public boolean push(int index, String match) { stack.push( new StackedOperation( index, match ) ); return true; } public StackedOperation pop() { return stack.pop(); } }