All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.lightblue.mongo.crud.js.JSQueryTranslator Maven / Gradle / Ivy

/*
 Copyright 2013 Red Hat, Inc. and/or its affiliates.

 This file is part of lightblue.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see .
 */
package com.redhat.lightblue.mongo.crud.js;

import java.math.BigInteger;
import java.math.BigDecimal;

import java.util.Map;

import org.bson.FieldNameValidator;

import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.text.SimpleDateFormat;

import com.redhat.lightblue.metadata.EntityMetadata;
import com.redhat.lightblue.metadata.FieldTreeNode;
import com.redhat.lightblue.metadata.ArrayField;
import com.redhat.lightblue.metadata.Type;
import com.redhat.lightblue.metadata.types.*;

import com.redhat.lightblue.query.*;

import com.redhat.lightblue.util.Error;
import com.redhat.lightblue.util.MutablePath;
import com.redhat.lightblue.util.Path;

/**
 * This class translates a query to javascript. It is used to
 * translate nontrivial query expressions to be used under a $where
 * construct
 */
public class JSQueryTranslator {

    public static final String ERR_INVALID_COMPARISON = "mongo-translation:invalid-comparison";
    public static final String ERR_INVALID_FIELD = "mongo-translation:invalid-field";

    private final EntityMetadata md;
    private static final SimpleDateFormat ISODATE_FORMAT=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");  
    private static final Map BINARY_COMPARISON_OPERATOR_JS_MAP;
    
    static{
        BINARY_COMPARISON_OPERATOR_JS_MAP = new HashMap<>();
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._eq, "==");
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._neq, "!=");
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._lt, "<");
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._gt, ">");
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._lte, "<=");
        BINARY_COMPARISON_OPERATOR_JS_MAP.put(BinaryComparisonOperator._gte, ">=");
    }
    
    public JSQueryTranslator(EntityMetadata md) {
        this.md=md;
    }
    
    public Expression translateQuery(QueryExpression query) {
        Context ctx=new Context(md.getFieldTreeRoot(),null);
        ctx.topLevel=new Function();
        ctx.topLevel.block=translateQuery(ctx,query);
        return ctx.topLevel;
    }

    private Block translateQuery(Context context,QueryExpression query) {
        Block ret=null;
        if (query instanceof ArrayContainsExpression) {
            ret = translateArrayContainsExpression(context, (ArrayContainsExpression) query);
        } else if (query instanceof ArrayMatchExpression) {
            ret = translateArrayElemMatch(context, (ArrayMatchExpression) query);
        } else if (query instanceof FieldComparisonExpression) {
            ret = translateFieldComparison(context, (FieldComparisonExpression) query);
        } else if (query instanceof NaryLogicalExpression) {
            ret = translateNaryLogicalExpression(context, (NaryLogicalExpression) query);
        } else if (query instanceof NaryValueRelationalExpression) {
            ret = translateNaryValueRelationalExpression(context, (NaryValueRelationalExpression) query);
        } else if (query instanceof NaryFieldRelationalExpression) {
            ret = translateNaryFieldRelationalExpression(context, (NaryFieldRelationalExpression) query);
        } else if (query instanceof RegexMatchExpression) {
            ret = translateRegexMatchExpression(context, (RegexMatchExpression) query);
        } else if (query instanceof UnaryLogicalExpression) {
            ret = translateUnaryLogicalExpression(context, (UnaryLogicalExpression) query);
        } else {
            ret = translateValueComparisonExpression(context, (ValueComparisonExpression) query);
        }
        return ret;
    }

    /**
     * If field1 and field2 are both non-arrays:
     * 
     *    result=field1 op field2
     * 
* * If field1 is an array and field2 is not: *
     *    result=false;
     *    for(i=0;i
     *
     * If both field1 and field2 are arrays:
     * 
     *    op=cmp:
     *  result=false;
     *  if(this.field1.length==this.field2.length) {
     *     result=true;
     *     for(int i=0;i
     *
     * If field1 has n ANYs and field2 has none:
     * 
     *    for(var i1=0;i1
     *
     * If field1 has n ANYs and field2 has m anys:
     * 
     *    for(var i1=0;i1
     */
    private Block translateFieldComparison(Context ctx,FieldComparisonExpression query) {
        Path rField = query.getRfield();
        FieldTreeNode rFieldMd = ctx.contextNode.resolve(rField);
        Path lField = query.getField();
        FieldTreeNode lFieldMd = ctx.contextNode.resolve(lField);

        Block comparisonBlock=new Block(ctx.topLevel.newGlobalBoolean(ctx));
        Block parentBlock=comparisonBlock;

        Name lfieldLocalName=new Name();
        // First deal with the nested arrays of lField
        parentBlock=processNestedArrays(ctx,lField,parentBlock,lfieldLocalName);
        
        // Then deal with the nested arrays of rField
        Name rfieldLocalName=new Name();
        parentBlock=processNestedArrays(ctx,rField,parentBlock,rfieldLocalName);

        Name lfieldName=ctx.varName(lfieldLocalName);
        Name rfieldName=ctx.varName(rfieldLocalName);
        
        if(rFieldMd instanceof ArrayField && lFieldMd instanceof ArrayField) {
            String loopVar=ctx.newName("i");
            // Both fields are arrays
            if(query.getOp()==BinaryComparisonOperator._neq) {
                parentBlock.add(new SimpleStatement("%s=true",comparisonBlock.resultVar));
                SimpleExpression cmp=new SimpleExpression(((ArrayField)rFieldMd).getElement().getType() instanceof DateType &&
                                                          ((ArrayField)lFieldMd).getElement().getType() instanceof DateType?
                                                          "this.%s[%s].valueOf()!=this.%s[%s].valueOf()":
                                                          "this.%s[%s]!=this.%s[%s]",
                                                          lfieldName,loopVar,
                                                          rfieldName,loopVar);
                parentBlock.add(IfStatement.ifDefined(lfieldName,rfieldName,
                                                      new IfStatement(new SimpleExpression("this.%s.length==this.%s.length",lfieldName,rfieldName),
                                                      new SimpleStatement("%s=false",comparisonBlock.resultVar),
                                                      new ForLoop(loopVar,true,lfieldName.toString(),
                                                                  new Block(new IfStatement(cmp,
                                                                                            new SimpleStatement("%s=true",comparisonBlock.resultVar),
                                                                                            SimpleStatement.S_BREAK))))));
            } else {
                SimpleExpression cmp=new SimpleExpression(((ArrayField)rFieldMd).getElement().getType() instanceof DateType &&
                                                          ((ArrayField)lFieldMd).getElement().getType() instanceof DateType?
                                                          "!(this.%s[%s].valueOf() %s this.%s[%s].valueOf())":
                                                          "!(this.%s[%s] %s this.%s[%s])",
                                                          lfieldName,loopVar,
                                                          BINARY_COMPARISON_OPERATOR_JS_MAP.get(query.getOp()),
                                                          rfieldName,loopVar);
                parentBlock.add(IfStatement.ifDefined(lfieldName,rfieldName,
                                                      new IfStatement(new SimpleExpression("this.%s.length==this.%s.length",lfieldName,rfieldName),
                                                                      new SimpleStatement("%s=true",comparisonBlock.resultVar),
                                                                      new ForLoop(loopVar,true,lfieldName.toString(),
                                                                                  new Block(new IfStatement(cmp,
                                                                                            new SimpleStatement("%s=false",comparisonBlock.resultVar),
                                                                                                            SimpleStatement.S_BREAK))))));
            }                                        
        } else if(rFieldMd instanceof ArrayField || lFieldMd instanceof ArrayField) {
            // Only one field is an array. If comparison is true for one element, then it is true
            Name arrayFieldName;
            Name simpleFieldName;
            BinaryComparisonOperator op;
            boolean isDate;
            if(rFieldMd instanceof ArrayField) {
                arrayFieldName=rfieldName;
                simpleFieldName=lfieldName;
                op=query.getOp().invert();
                isDate=((ArrayField)rFieldMd).getElement().getType() instanceof DateType && lFieldMd.getType() instanceof DateType;
            } else {
                arrayFieldName=lfieldName;
                simpleFieldName=rfieldName;
                op=query.getOp();
                isDate=((ArrayField)lFieldMd).getElement().getType() instanceof DateType && rFieldMd.getType() instanceof DateType;
            }
            String loopVar=ctx.newName("i");
            SimpleExpression cmp=new SimpleExpression(isDate?"this.%s[%s].valueOf() %s this.%s.valueOf()":"this.%s[%s] %s this.%s",
                                                      arrayFieldName,loopVar,
                                                      BINARY_COMPARISON_OPERATOR_JS_MAP.get(op),
                                                      simpleFieldName);
            parentBlock.add(new ForLoop(loopVar,true,arrayFieldName.toString(),
                                        new Block(new IfStatement(cmp,
                                                                  new SimpleStatement("%s=true",comparisonBlock.resultVar),
                                                                  SimpleStatement.S_BREAK))));
        } else {
            // Simple comparison
            parentBlock.add(IfStatement.ifDefined(lfieldName,rfieldName,
                                                  new SimpleStatement("%s=this.%s %s this.%s",comparisonBlock.resultVar,lfieldName,BINARY_COMPARISON_OPERATOR_JS_MAP.get(query.getOp()),rfieldName)));
        }
        // Add breaks to the end of for loops
        // Trace ctx back to ctx, add breaks to for loops
        IfStatement breakIfNecessary=new IfStatement(new SimpleExpression(comparisonBlock.resultVar),
                                                     SimpleStatement.S_BREAK);
        Statement parentStmt=parentBlock;
        while(parentStmt!=comparisonBlock) {            
            if(parentStmt instanceof ForLoop)
                ((ForLoop)parentStmt).add(breakIfNecessary);
            parentStmt=((Statement)parentStmt).parent;
        }
        
        return comparisonBlock;
        
    }

    private Block processNestedArrays(Context ctx,Path field,Block parent,Name arrayFieldName) {
        MutablePath pathSegment=new MutablePath();
        for(int i=0;i
     * in:
     * var r0=false;
     * nin:
     * var r=true;
     * for(var i=0;i
     */
    private Block translateNaryFieldRelationalExpression(Context ctx,NaryFieldRelationalExpression query) {
        FieldTreeNode fieldMd=ctx.contextNode.resolve(query.getField());
        if(!fieldMd.getType().supportsEq()) {
            throw Error.get(ERR_INVALID_COMPARISON, query.toString());            
        }
        Block block=new Block(ctx.topLevel.newGlobal(ctx,query.getOp()==NaryRelationalOperator._in?"false":"true"));
        Name rname=ctx.varName(new Name(query.getRfield()));
        Name lname=ctx.varName(new Name(query.getField()));
        ArrForLoop loop=new ArrForLoop(ctx.newName("i"),rname);
        block.add(IfStatement.ifDefined(rname,lname,loop));
        SimpleExpression cmpExpression=new SimpleExpression(fieldMd.getType() instanceof DateType?
                                                            "this.%s[%s].valueOf()==this.%s.valueOf()":
                                                            "this.%s[%s]==this.%s",rname,
                                                            loop.loopVar,
                                                            lname);
        loop.add(new IfStatement(cmpExpression,
                                 new SimpleStatement("%s=%s",block.resultVar,query.getOp()==NaryRelationalOperator._in?"true":"false"),
                                 SimpleStatement.S_BREAK));
        return block;
    }
    
    /**
     * 
     * var r0=[values];
     * any:
     * var r1=false;
     * all: none:
     * var r1=true;
     * var r3=false;
     * for(var i=0;i
     */
    private Block translateArrayContainsExpression(Context ctx,ArrayContainsExpression query) {
        FieldTreeNode fieldMd=ctx.contextNode.resolve(query.getArray());
        String valueArr=declareValueArray(ctx,((ArrayField)fieldMd).getElement(),query.getValues());
        Block arrayContainsBlock=new Block(ctx.topLevel.newGlobal(ctx,query.getOp()==ContainsOperator._any?"false":"true"));


        // for(var i=0;i
     *   var arr=[values];
     * 
*/ private String declareValueArray(Context ctx,FieldTreeNode fieldMd,List values) { StringBuilder arr=new StringBuilder(); arr.append('['); boolean first=true; for(Value v:values) { Object value=filterBigNumbers(fieldMd.getType().cast(v.getValue())); if(first) { first=false; } else { arr.append(','); } if(value==null) arr.append("null"); else { if(fieldMd.getType() instanceof DateType) { arr.append(String.format("ISODate('%s')",toISODate((Date)value))); } else { arr.append(quote(fieldMd.getType(),value.toString())); } } } arr.append(']'); return ctx.topLevel.newGlobal(ctx,arr.toString()); } /** *
     * var r0=[values];
     * var r1=false;
     * for(var i=0;i
     */
    private Block translateNaryValueRelationalExpression(Context ctx,NaryValueRelationalExpression query) {
        FieldTreeNode fieldMd=ctx.contextNode.resolve(query.getField());
        String globalArr=declareValueArray(ctx,fieldMd,query.getValues());
        Block block=new Block(ctx.topLevel.newGlobal(ctx,query.getOp()==NaryRelationalOperator._in?"false":"true"));
        String loopVar=ctx.newName("i");
        ForLoop forLoop=new ForLoop(loopVar,globalArr);
        Name fieldName=ctx.varName(new Name(query.getField()));
        block.add(IfStatement.ifDefined(fieldName,forLoop));
        if(fieldMd.getType() instanceof DateType) {
            if(query.getOp()==NaryRelationalOperator._in) {
                forLoop.add(new IfStatement(new SimpleExpression("this.%s.valueOf()==this.%s[%s].valueOf()",fieldName,globalArr,loopVar),
                                            new SimpleStatement("%s=true",block.resultVar),
                                            SimpleStatement.S_BREAK));
            } else {
                forLoop.add(new IfStatement(new SimpleExpression("this.%s.valueOf()==this.%s[%s].valueOf()",fieldName,globalArr,loopVar),
                                            new SimpleStatement("%s=false",block.resultVar),
                                            SimpleStatement.S_BREAK));
            }
        } else {
            if(query.getOp()==NaryRelationalOperator._in) {
                forLoop.add(new IfStatement(new SimpleExpression("this.%s==%s[%s]",fieldName,globalArr,loopVar),
                                            new SimpleStatement("%s=true",block.resultVar),
                                            SimpleStatement.S_BREAK));
            } else {
                forLoop.add(new IfStatement(new SimpleExpression("this.%s==%s[%s]",fieldName,globalArr,loopVar),
                                            new SimpleStatement("%s=false",block.resultVar),
                                            SimpleStatement.S_BREAK));
            }
        }        
        
        return block;
    }

    /**
     *  
     *   var r0=new RegExp("pattern","options");
     *   var r1=false;
     *   r1=r0.test(field);
     *   return r1;
     * 
*/ private Block translateRegexMatchExpression(Context ctx,RegexMatchExpression query) { FieldTreeNode fieldMd=ctx.contextNode.resolve(query.getField()); if(fieldMd.getType() instanceof StringType) { Name fieldName=ctx.varName(new Name(query.getField())); String regexVar=ctx.topLevel.newGlobal(ctx,String.format("new RegExp(\"%s\",\"%s\")", query.getRegex().replaceAll("\"","\\\""), regexFlags(query))); Block block=new Block(ctx.topLevel.newGlobalBoolean(ctx)); block.add(IfStatement.ifDefined(fieldName,new SimpleStatement("%s=%s.test(this.%s)", block.resultVar, regexVar, fieldName.toString()))); return block; } else throw Error.get(ERR_INVALID_COMPARISON,query.toString()); } private String regexFlags(RegexMatchExpression query) { StringBuilder bld=new StringBuilder(); if(query.isCaseInsensitive()) bld.append('i'); if(query.isMultiline()) bld.append('m'); return bld.toString(); } /** *
     *   var r0=false;
     *   r0=this.field == value
     *   return r0
     * 
*/ private Block translateValueComparisonExpression(Context ctx,ValueComparisonExpression query) { FieldTreeNode fieldMd=ctx.contextNode.resolve(query.getField()); Object value = query.getRvalue().getValue(); if (query.getOp() == BinaryComparisonOperator._eq || query.getOp() == BinaryComparisonOperator._neq) { if (!fieldMd.getType().supportsEq() && value != null) { throw Error.get(ERR_INVALID_COMPARISON, query.toString()); } } else if (!fieldMd.getType().supportsOrdering()) { throw Error.get(ERR_INVALID_COMPARISON, query.toString()); } Object valueObject = filterBigNumbers(fieldMd.getType().cast(value)); Name fieldName=ctx.varName(new Name(query.getField())); Block block=new Block(ctx.topLevel.newGlobalBoolean(ctx)); if(valueObject!=null&&fieldMd.getType() instanceof DateType) { block.add(IfStatement.ifDefined(fieldName,new SimpleStatement("%s=this.%s.valueOf() %s ISODate('%s').valueOf()",block.resultVar,fieldName.toString(), BINARY_COMPARISON_OPERATOR_JS_MAP.get(query.getOp()),toISODate((Date)valueObject)))); } else { block.add(IfStatement.ifDefined(fieldName,new SimpleStatement("%s=this.%s %s %s",block.resultVar, fieldName.toString(), BINARY_COMPARISON_OPERATOR_JS_MAP.get(query.getOp()), valueObject==null?"null":quote(fieldMd.getType(),valueObject.toString())))); } return block; } /** *
     *   var r0=false;
     *   for(var i=0;i
     */
    private Block translateArrayElemMatch(Context ctx,ArrayMatchExpression query) {
        // An elemMatch expression is a for-loop
        Block block=new Block(ctx.topLevel.newGlobalBoolean(ctx));

        // for (elem:array)
        Name arrName=ctx.varName(new Name(query.getArray()));
        ArrForLoop loop=new ArrForLoop(ctx.newName("i"),arrName);
        Context newCtx=ctx.enter(ctx.contextNode.resolve(new Path(query.getArray(),Path.ANYPATH)),loop);
        Block queryBlock=translateQuery(newCtx,query.getElemMatch());
        loop.replace(queryBlock);
        loop.add(new IfStatement(new SimpleExpression(queryBlock.resultVar),
                                 new SimpleStatement("%s=true",block.resultVar),
                                 SimpleStatement.S_BREAK));
        block.add(IfStatement.ifDefined(arrName,loop));
        return block;
    }

    /**
     * 
     *    var r0=false;
     *    {
     *       q1;
     *       q2;...
     *    }
     *    r0=r1&&r2&&...
     *    return r0;
     * 
*/ private Block translateNaryLogicalExpression(Context ctx,NaryLogicalExpression query) { Block block=new Block(ctx.topLevel.newGlobalBoolean(ctx)); List vars=new ArrayList(); for(QueryExpression x:query.getQueries()) { Block nested=translateQuery(ctx,x); vars.add(nested.resultVar); block.add(nested); } String op=query.getOp()==NaryLogicalOperator._and?"&&":"||"; block.add(new SimpleStatement("%s=%s",block.resultVar,String.join(op,vars))); return block; } /** *
     *   var r0=false;
     *   q;
     *   r0=!q
     *   return r0;
     * 
*/ private Block translateUnaryLogicalExpression(Context ctx,UnaryLogicalExpression query) { // Only NOT is a unary operator Block block=new Block(ctx.topLevel.newGlobalBoolean(ctx)); Block nested=translateQuery(ctx,query.getQuery()); block.add(nested); block.add(new SimpleStatement("%s=!%s",block.resultVar,nested.resultVar)); return block; } private String quote(Type t,String value) { if(t instanceof StringType||t instanceof BigDecimalType|| t instanceof BigIntegerType) return String.format("\"%s\"",value); else return value; } private Object filterBigNumbers(Object value) { // Store big values as string. Mongo does not support big values if (value instanceof BigDecimal || value instanceof BigInteger) { return value.toString(); } else { return value; } } private String toISODate(Date d) { return ((SimpleDateFormat)ISODATE_FORMAT.clone()).format(d); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy