Browse Source

AMBARI-1294. Add 'isEmpty' and 'in' query operators to API

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/trunk@1441233 13f79535-47bb-0310-9956-ffa450edef68
John Speidel 12 năm trước cách đây
mục cha
commit
d94b27e3f8
16 tập tin đã thay đổi với 404 bổ sung40 xóa
  1. 2 0
      CHANGES.txt
  2. 36 8
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/QueryLexer.java
  3. 115 18
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/QueryParser.java
  4. 2 0
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/Token.java
  5. 2 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/Expression.java
  6. 2 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/LogicalExpression.java
  7. 2 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/NotLogicalExpression.java
  8. 2 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/RelationalExpression.java
  9. 54 0
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/InOperator.java
  10. 51 0
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/IsEmptyOperator.java
  11. 3 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/Operator.java
  12. 3 1
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/RelationalOperator.java
  13. 10 5
      ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/RelationalOperatorFactory.java
  14. 5 1
      ambari-server/src/test/java/org/apache/ambari/server/api/TestSuite.java
  15. 27 1
      ambari-server/src/test/java/org/apache/ambari/server/api/predicate/QueryLexerTest.java
  16. 88 1
      ambari-server/src/test/java/org/apache/ambari/server/api/predicate/QueryParserTest.java

+ 2 - 0
CHANGES.txt

@@ -12,6 +12,8 @@ Trunk (unreleased changes):
 
  NEW FEATURES
 
+ AMBARI-1294. Add isEmpty() query operator support. (jspeidel)
+
  AMBARI-1280. Support explicit predicate grouping in API queries. (jspeidel)
 
  AMBARI-1180. Display host check status results given by the agent as part

+ 36 - 8
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/QueryLexer.java

@@ -30,13 +30,14 @@ import java.util.regex.Pattern;
  * a regular expression which splits on a set of deliminators which includes
  * operators and brackets.
  *
- * Second, each string token is converted into a Token with type an value information.
+ * Second, each string token is converted into a Token with type and value information.
  */
 public class QueryLexer {
   /**
    * All valid deliminators.
    */
-  private static final String[] ALL_DELIMS = {"<=",">=","!=","=","<",">","&","|","!","(", ")"};
+  private static final String[] ALL_DELIMS =
+      {".in\\(",".isEmpty\\(","<=",">=","!=","=","<",">","&","|","!","(", ")"};
 
   /**
    * Map of token type to list of valid handlers for next token.
@@ -54,6 +55,7 @@ public class QueryLexer {
    * Register token handlers.
    */
   public QueryLexer() {
+    //todo: refactor handler registration
     List<TokenHandler> listHandlers = new ArrayList<TokenHandler>();
     listHandlers.add(new LogicalUnaryOperatorTokenHandler());
     listHandlers.add(new OpenBracketTokenHandler());
@@ -65,12 +67,18 @@ public class QueryLexer {
 
     listHandlers= new ArrayList<TokenHandler>();
     listHandlers.add(new RelationalOperatorTokenHandler());
+    listHandlers.add(new RelationalOperatorFuncTokenHandler());
     TOKEN_HANDLERS.put(Token.TYPE.PROPERTY_OPERAND, listHandlers);
 
     listHandlers = new ArrayList<TokenHandler>();
     listHandlers.add(new ValueOperandTokenHandler());
     TOKEN_HANDLERS.put(Token.TYPE.RELATIONAL_OPERATOR, listHandlers);
 
+    listHandlers = new ArrayList<TokenHandler>();
+    listHandlers.add(new CloseBracketTokenHandler());
+    listHandlers.add(new ValueOperandTokenHandler());
+    TOKEN_HANDLERS.put(Token.TYPE.RELATIONAL_OPERATOR_FUNC, listHandlers);
+
     listHandlers = new ArrayList<TokenHandler>();
     listHandlers.add(new CloseBracketTokenHandler());
     listHandlers.add(new LogicalOperatorTokenHandler());
@@ -90,7 +98,6 @@ public class QueryLexer {
   public Token[] tokens(String exp) throws InvalidQueryException {
 
     ScanContext ctx = new ScanContext();
-
     for (String tok : parseStringTokens(exp)) {
       List<TokenHandler> listHandlers = TOKEN_HANDLERS.get(ctx.getLastTokenType());
       boolean            processed    = false;
@@ -105,7 +112,6 @@ public class QueryLexer {
             tok + "\', previous token type=" + ctx.getLastTokenType());
       }
     }
-
     return ctx.getTokenList().toArray(new Token[ctx.getTokenList().size()]);
   }
 
@@ -267,7 +273,6 @@ public class QueryLexer {
     }
   }
 
-
   /**
    * Token handler base class.
    * Token handlers are responsible for processing specific token type.
@@ -365,7 +370,7 @@ public class QueryLexer {
 
     @Override
     public boolean handles(String token, Token.TYPE previousTokenType) {
-      return token.matches("[^!&\\|<=|>=|!=|=|<|>\\(\\)]+");
+      return token.matches("[^!&\\|<=|>=|!=|=|<|>]+");
     }
   }
 
@@ -390,7 +395,7 @@ public class QueryLexer {
   }
 
   /**
-   * Close Bracker token handler.
+   * Close Bracket token handler.
    */
   private class CloseBracketTokenHandler extends TokenHandler {
     @Override
@@ -414,7 +419,7 @@ public class QueryLexer {
    */
   private class RelationalOperatorTokenHandler extends TokenHandler {
     @Override
-  public void _handleToken(String token, ScanContext ctx) throws InvalidQueryException {
+    public void _handleToken(String token, ScanContext ctx) throws InvalidQueryException {
       ctx.addToken(new Token(Token.TYPE.RELATIONAL_OPERATOR, token));
       ctx.addToken(new Token(Token.TYPE.PROPERTY_OPERAND, ctx.getPropertyOperand()));
     }
@@ -430,6 +435,29 @@ public class QueryLexer {
     }
   }
 
+  /**
+   * Relational Operator function token handler.
+   */
+  private class RelationalOperatorFuncTokenHandler extends TokenHandler {
+    @Override
+    public void _handleToken(String token, ScanContext ctx) throws InvalidQueryException {
+      ctx.addToken(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, token));
+      ctx.addToken(new Token(Token.TYPE.PROPERTY_OPERAND, ctx.getPropertyOperand()));
+    }
+
+    @Override
+    public Token.TYPE getType() {
+      return Token.TYPE.RELATIONAL_OPERATOR_FUNC;
+    }
+
+    //todo: add a unary relational operator func
+    @Override
+    public boolean handles(String token, Token.TYPE previousTokenType) {
+      return token.matches("\\.[a-zA-Z]+\\(");
+    }
+  }
+
+
   /**
    * Logical Operator token handler.
    */

+ 115 - 18
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/QueryParser.java

@@ -51,6 +51,7 @@ public class QueryParser {
     TOKEN_HANDLERS.put(Token.TYPE.LOGICAL_UNARY_OPERATOR, new LogicalUnaryOperatorTokenHandler());
     TOKEN_HANDLERS.put(Token.TYPE.PROPERTY_OPERAND, new PropertyOperandTokenHandler());
     TOKEN_HANDLERS.put(Token.TYPE.VALUE_OPERAND, new ValueOperandTokenHandler());
+    TOKEN_HANDLERS.put(Token.TYPE.RELATIONAL_OPERATOR_FUNC, new RelationalOperatorFuncTokenHandler());
   }
 
   /**
@@ -93,10 +94,10 @@ public class QueryParser {
    * @throws InvalidQueryException if unable to properly parse the tokens into a parse context
    */
   private ParseContext parseExpressions(Token[] tokens) throws InvalidQueryException {
-    ParseContext ctx = new ParseContext();
+    ParseContext ctx = new ParseContext(tokens);
 
-    for (Token token : tokens) {
-      TOKEN_HANDLERS.get(token.getType()).handleToken(token, ctx);
+    while (ctx.getCurrentTokensIndex() < tokens.length) {
+      TOKEN_HANDLERS.get(tokens[ctx.getCurrentTokensIndex()].getType()).handleToken(ctx);
     }
 
     if (ctx.getPrecedenceLevel() != 0) {
@@ -128,10 +129,8 @@ public class QueryParser {
         stack.addAll(exp.merge(left, right, precedenceLevel));
       }
       return mergeExpressions(new ArrayList<Expression>(stack), precedenceLevel - 1);
-
-    } else {
-      return listExpressions;
     }
+    return listExpressions;
   }
 
   /**
@@ -143,6 +142,16 @@ public class QueryParser {
      */
     private int m_precedence = 0;
 
+    /**
+     * Current position in tokens array
+     */
+    private int m_tokensIdx = 0;
+
+    /**
+     * Tokens
+     */
+    private Token[] m_tokens;
+
     /**
      * The type of the previous token used in validation.
      */
@@ -158,6 +167,35 @@ public class QueryParser {
      */
     int m_maxPrecedence = 0;
 
+    public ParseContext(Token[] tokens) {
+      m_tokens = tokens;
+    }
+
+    /**
+     * Get array of all tokens.
+     * @return token array
+     */
+    public Token[] getTokens() {
+      return m_tokens;
+    }
+
+    /**
+     * Get the current position in the tokens array.
+     * @return the current tokens index
+     */
+    public int getCurrentTokensIndex() {
+      return m_tokensIdx;
+    }
+
+    /**
+     * Set the current position in the tokens array.
+     * Each handler should set this value after processing a token(s).
+     * @param idx  current tokens index
+     */
+    public void setCurrentTokensIndex(int idx) {
+      m_tokensIdx = idx;
+    }
+
     /**
      * Increment the context precedence level.
      *
@@ -267,28 +305,29 @@ public class QueryParser {
      * Process a token. Handles common token processing functionality then delegates to the individual
      * concrete handlers.
      *
-     * @param token  the token to process
      * @param ctx    the current parse context
      * @throws InvalidQueryException if unable to process the token
      */
-    public void handleToken(Token token, ParseContext ctx) throws InvalidQueryException {
+    public void handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token token = ctx.getTokens()[ctx.getCurrentTokensIndex()];
       if (! validate(ctx.getPreviousTokenType())) {
         throw new InvalidQueryException("Unexpected token encountered in query string. Last Token Type=" +
             ctx.getPreviousTokenType() + ", Current Token[type=" + token.getType() +
             ", value='" + token.getValue() + "']");
       }
       ctx.setTokenType(token.getType());
-      _handleToken(token, ctx);
+
+      int idxIncrement = _handleToken(ctx);
+      ctx.setCurrentTokensIndex(ctx.getCurrentTokensIndex() + idxIncrement);
     }
 
     /**
      * Process a token.
      *
-     * @param token  the token to process
      * @param ctx    the current parse context
      * @throws InvalidQueryException if unable to process the token
      */
-    public abstract void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException;
+    public abstract int _handleToken(ParseContext ctx) throws InvalidQueryException;
 
     /**
      * Validate the token based on the previous token.
@@ -305,8 +344,9 @@ public class QueryParser {
   private class BracketOpenTokenHandler extends TokenHandler {
 
     @Override
-    public void _handleToken(Token token, ParseContext ctx) {
+    public int _handleToken(ParseContext ctx) {
       ctx.incPrecedenceLevel(Operator.MAX_OP_PRECEDENCE);
+      return 1;
     }
 
     @Override
@@ -323,8 +363,10 @@ public class QueryParser {
    */
   private class BracketCloseTokenHandler extends TokenHandler {
     @Override
-    public void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException{
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException{
       ctx.decPrecedenceLevel(Operator.MAX_OP_PRECEDENCE);
+
+      return 1;
     }
 
     @Override
@@ -339,10 +381,55 @@ public class QueryParser {
    */
   private class RelationalOperatorTokenHandler extends TokenHandler {
     @Override
-    public void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException {
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token token = ctx.getTokens()[ctx.getCurrentTokensIndex()];
       RelationalOperator relationalOp = RelationalOperatorFactory.createOperator(token.getValue());
       //todo: use factory to create expression
       ctx.addExpression(new RelationalExpression(relationalOp));
+
+      return 1;
+    }
+
+    @Override
+    public boolean validate(Token.TYPE previousTokenType) {
+      return previousTokenType == null                     ||
+          previousTokenType == Token.TYPE.BRACKET_OPEN     ||
+          previousTokenType == Token.TYPE.LOGICAL_OPERATOR ||
+          previousTokenType == Token.TYPE.LOGICAL_UNARY_OPERATOR;
+    }
+  }
+
+  /**
+   * Relational Operator function token handler
+   */
+  private class RelationalOperatorFuncTokenHandler extends TokenHandler {
+    @Override
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token[]            tokens       = ctx.getTokens();
+      int                idx          = ctx.getCurrentTokensIndex();
+      Token              token        = tokens[idx];
+      RelationalOperator relationalOp = RelationalOperatorFactory.createOperator(token.getValue());
+
+      ctx.addExpression(new RelationalExpression(relationalOp));
+      ctx.setCurrentTokensIndex(++idx);
+
+      TokenHandler propertyHandler = new PropertyOperandTokenHandler();
+      propertyHandler.handleToken(ctx);
+
+      // handle right operand if applicable to operator
+      idx = ctx.getCurrentTokensIndex();
+      if (ctx.getCurrentTokensIndex() < tokens.length &&
+          tokens[idx].getType().equals(Token.TYPE.VALUE_OPERAND)) {
+        TokenHandler valueHandler = new ValueOperandTokenHandler();
+        valueHandler.handleToken(ctx);
+      }
+
+      // skip closing bracket
+      idx = ctx.getCurrentTokensIndex();
+      if (idx >= tokens.length || tokens[idx].getType() != Token.TYPE.BRACKET_CLOSE) {
+        throw new InvalidQueryException("Missing closing bracket for in expression.") ;
+      }
+      return 1;
     }
 
     @Override
@@ -359,10 +446,13 @@ public class QueryParser {
    */
   private class LogicalOperatorTokenHandler extends TokenHandler {
     @Override
-    public void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException {
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token token = ctx.getTokens()[ctx.getCurrentTokensIndex()];
       LogicalOperator logicalOp = LogicalOperatorFactory.createOperator(token.getValue(), ctx.getPrecedenceLevel());
       ctx.updateMaxPrecedence(logicalOp.getPrecedence());
       ctx.addExpression(LogicalExpressionFactory.createLogicalExpression(logicalOp));
+
+      return 1;
     }
 
     @Override
@@ -389,13 +479,17 @@ public class QueryParser {
    */
   private class PropertyOperandTokenHandler extends TokenHandler {
     @Override
-    public void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException {
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token token = ctx.getTokens()[ctx.getCurrentTokensIndex()];
       ctx.getPrecedingExpression().setLeftOperand(token.getValue());
+
+      return 1;
     }
 
     @Override
     public boolean validate(Token.TYPE previousTokenType) {
-      return previousTokenType == Token.TYPE.RELATIONAL_OPERATOR;
+      return previousTokenType == Token.TYPE.RELATIONAL_OPERATOR ||
+          previousTokenType == Token.TYPE.RELATIONAL_OPERATOR_FUNC;
     }
   }
 
@@ -404,8 +498,11 @@ public class QueryParser {
    */
   private class ValueOperandTokenHandler extends TokenHandler {
     @Override
-    public void _handleToken(Token token, ParseContext ctx) throws InvalidQueryException {
+    public int _handleToken(ParseContext ctx) throws InvalidQueryException {
+      Token token = ctx.getTokens()[ctx.getCurrentTokensIndex()];
       ctx.getPrecedingExpression().setRightOperand(token.getValue());
+
+      return 1;
     }
 
     @Override

+ 2 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/Token.java

@@ -34,6 +34,8 @@ public class Token {
     VALUE_OPERAND,
     /** Relational operator */
     RELATIONAL_OPERATOR,
+    /** Relational operator function */
+    RELATIONAL_OPERATOR_FUNC,
     /** Logical operator */
     LOGICAL_OPERATOR,
     /** Logical unary operator such as !*/

+ 2 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/Expression.java

@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.api.predicate.expressions;
 
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
 import org.apache.ambari.server.api.predicate.operators.Operator;
 import org.apache.ambari.server.controller.spi.Predicate;
 
@@ -49,7 +50,7 @@ public interface Expression<T> {
    * Get the predicate representation of the expression.
    * @return a predicate instance for the expression
    */
-  public Predicate toPredicate();
+  public Predicate toPredicate() throws InvalidQueryException;
 
   /**
    * Set the expressions left operand.

+ 2 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/LogicalExpression.java

@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.api.predicate.expressions;
 
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
 import org.apache.ambari.server.api.predicate.operators.LogicalOperator;
 import org.apache.ambari.server.controller.spi.Predicate;
 
@@ -42,7 +43,7 @@ public class LogicalExpression extends AbstractExpression<Expression> {
 
 
   @Override
-  public Predicate toPredicate() {
+  public Predicate toPredicate() throws InvalidQueryException {
     return ((LogicalOperator) getOperator()).
         toPredicate(getLeftOperand().toPredicate(), getRightOperand().toPredicate());
   }

+ 2 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/NotLogicalExpression.java

@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.api.predicate.expressions;
 
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
 import org.apache.ambari.server.api.predicate.operators.LogicalOperator;
 import org.apache.ambari.server.controller.predicate.BasePredicate;
 import org.apache.ambari.server.controller.predicate.NotPredicate;
@@ -57,7 +58,7 @@ public class NotLogicalExpression extends LogicalExpression {
   }
 
   @Override
-  public Predicate toPredicate() {
+  public Predicate toPredicate() throws InvalidQueryException {
     //todo: remove need to down cast to BasePredicate
     return new NotPredicate((BasePredicate) getRightOperand().toPredicate());
   }

+ 2 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/expressions/RelationalExpression.java

@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.api.predicate.expressions;
 
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
 import org.apache.ambari.server.api.predicate.operators.RelationalOperator;
 import org.apache.ambari.server.controller.spi.Predicate;
 
@@ -38,7 +39,7 @@ public class RelationalExpression extends AbstractExpression<String> {
   }
 
   @Override
-  public Predicate toPredicate() {
+  public Predicate toPredicate() throws InvalidQueryException {
     return ((RelationalOperator) getOperator()).
         toPredicate(getLeftOperand(), getRightOperand());
   }

+ 54 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/InOperator.java

@@ -0,0 +1,54 @@
+package org.apache.ambari.server.api.predicate.operators;
+
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
+import org.apache.ambari.server.controller.predicate.BasePredicate;
+import org.apache.ambari.server.controller.predicate.EqualsPredicate;
+import org.apache.ambari.server.controller.predicate.OrPredicate;
+import org.apache.ambari.server.controller.spi.Predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * IN relational operator.
+ * This is a binary operator which takes a comma delimited right operand and
+ * creates equals predicates with the left operand and each right operand token.
+ * The equals predicates are combined with an OR predicate.
+ *
+ */
+public class InOperator extends AbstractOperator implements RelationalOperator {
+
+  public InOperator() {
+    super(0);
+  }
+
+  @Override
+  public String getName() {
+    return "InOperator";
+  }
+
+  @Override
+  public Predicate toPredicate(String prop, String val) throws InvalidQueryException {
+
+    if (val == null) {
+      throw new InvalidQueryException("IN operator is missing a required right operand.");
+    }
+
+    String[] tokens = val.split(",");
+    List<EqualsPredicate> listPredicates = new ArrayList<EqualsPredicate>();
+    for (String token : tokens) {
+      listPredicates.add(new EqualsPredicate(prop, token.trim()));
+    }
+    return listPredicates.size() == 1 ? listPredicates.get(0) :
+        buildOrPredicate(listPredicates);
+  }
+
+  private OrPredicate buildOrPredicate(List<EqualsPredicate> listPredicates) {
+    return new OrPredicate(listPredicates.toArray(new BasePredicate[listPredicates.size()]));
+  }
+
+  @Override
+  public TYPE getType() {
+    return TYPE.IN;
+  }
+}

+ 51 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/IsEmptyOperator.java

@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.ambari.server.api.predicate.operators;
+
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
+import org.apache.ambari.server.controller.predicate.CategoryIsEmptyPredicate;
+import org.apache.ambari.server.controller.spi.Predicate;
+
+/**
+ * Operator that is used to determine if a category is empty, meaning that it doesn't
+ * contain any properties.
+ */
+public class IsEmptyOperator extends AbstractOperator implements RelationalOperator {
+  public IsEmptyOperator() {
+    super(0);
+  }
+
+  @Override
+  public String getName() {
+    return "IsEmptyOperator";
+  }
+
+  @Override
+  public Predicate toPredicate(String prop, String val) throws InvalidQueryException {
+    if (val != null) {
+      throw new InvalidQueryException("'isEmpty' operator shouldn't have a right operand but one exists: " + val);
+    }
+    return new CategoryIsEmptyPredicate(prop);
+  }
+
+  @Override
+  public TYPE getType() {
+    return TYPE.IS_EMPTY;
+  }
+}

+ 3 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/Operator.java

@@ -35,7 +35,9 @@ public interface Operator {
     NOT_EQUAL,
     AND,
     OR,
-    NOT
+    NOT,
+    IN,
+    IS_EMPTY
   }
 
   /**

+ 3 - 1
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/RelationalOperator.java

@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.api.predicate.operators;
 
+import org.apache.ambari.server.api.predicate.InvalidQueryException;
 import org.apache.ambari.server.controller.spi.Predicate;
 
 /**
@@ -30,6 +31,7 @@ public interface RelationalOperator extends Operator {
    * @param prop  left operand
    * @param val   right operand
    * @return  a predicate instance for this operator.
+   * @throws  InvalidQueryException if unable to build the predicate because of invalid operands
    */
-  public Predicate toPredicate(String prop, String val);
+  public Predicate toPredicate(String prop, String val) throws InvalidQueryException;
 }

+ 10 - 5
ambari-server/src/main/java/org/apache/ambari/server/api/predicate/operators/RelationalOperatorFactory.java

@@ -29,7 +29,8 @@ public class RelationalOperatorFactory {
    * of the operator.
    *
    * @param operator  the string representation of the operator
-   * @return relationl operator for the given string
+   *
+   * @return relational operator for the given string
    * @throws InvalidQueryException if an invalid operator is passed in
    */
   public static RelationalOperator createOperator(String operator) throws InvalidQueryException {
@@ -37,14 +38,18 @@ public class RelationalOperatorFactory {
       return new NotEqualsOperator();
     } else if ("=".equals(operator)) {
       return new EqualsOperator();
-    } if ("<=".equals(operator)) {
+    } else if ("<=".equals(operator)) {
       return new LessEqualsOperator();
-    } if ("<".equals(operator)) {
+    } else if ("<".equals(operator)) {
       return new LessOperator();
-    }else if (">=".equals(operator)) {
+    } else if (">=".equals(operator)) {
       return new GreaterEqualsOperator();
-    } if (">".equals(operator)) {
+    } else if (">".equals(operator)) {
       return new GreaterOperator();
+    } else if (".in(".equals(operator)) {
+      return new InOperator();
+    } else if (".isEmpty(".equals(operator)) {
+      return new IsEmptyOperator();
     } else {
       throw new RuntimeException("Invalid Operator Type: " + operator);
     }

+ 5 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/TestSuite.java

@@ -25,6 +25,7 @@ package org.apache.ambari.server.api;
 import org.apache.ambari.server.api.handlers.*;
 import org.apache.ambari.server.api.predicate.QueryLexerTest;
 import org.apache.ambari.server.api.predicate.QueryParserTest;
+import org.apache.ambari.server.api.predicate.operators.*;
 import org.apache.ambari.server.api.query.QueryImplTest;
 import org.apache.ambari.server.api.resources.ResourceInstanceImplTest;
 import org.apache.ambari.server.api.services.*;
@@ -39,6 +40,9 @@ import org.junit.runners.Suite;
     JsonPropertyParserTest.class, CreateHandlerTest.class, UpdateHandlerTest.class, DeleteHandlerTest.class,
     PersistenceManagerImplTest.class, GetRequestTest.class, PutRequestTest.class, PostRequestTest.class,
     DeleteRequestTest.class, JsonSerializerTest.class, QueryCreateHandlerTest.class, ResourceInstanceImplTest.class,
-    QueryLexerTest.class, QueryParserTest.class})
+    QueryLexerTest.class, QueryParserTest.class, IsEmptyOperatorTest.class, InOperatorTest.class,
+    AndOperatorTest.class, OrOperatorTest.class, EqualsOperatorTest.class, GreaterEqualsOperatorTest.class,
+    GreaterOperatorTest.class, LessEqualsOperatorTest.class, LessEqualsOperatorTest.class, NotEqualsOperatorTest.class,
+    NotOperatorTest.class})
 public class TestSuite {
 }

+ 27 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/predicate/QueryLexerTest.java

@@ -84,14 +84,40 @@ public class QueryLexerTest {
 
   @Test
   public void testUnaryNot() throws Exception {
+    QueryLexer lexer = new QueryLexer();
+    Token[] tokens = lexer.tokens("!foo<5");
+
     List<Token> listTokens = new ArrayList<Token>();
     listTokens.add(new Token(Token.TYPE.LOGICAL_UNARY_OPERATOR, "!"));
     listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR, "<"));
     listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "foo"));
     listTokens.add(new Token(Token.TYPE.VALUE_OPERAND, "5"));
+    assertArrayEquals(listTokens.toArray(new Token[listTokens.size()]), tokens);
+  }
 
+  @Test
+  public void testInOperator() throws Exception {
     QueryLexer lexer = new QueryLexer();
-    Token[] tokens = lexer.tokens("!foo<5");
+    Token[] tokens = lexer.tokens("foo.in(one, two, 3)");
+
+    List<Token> listTokens = new ArrayList<Token>();
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".in("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "foo"));
+    listTokens.add(new Token(Token.TYPE.VALUE_OPERAND, "one, two, 3"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
+
+    assertArrayEquals(listTokens.toArray(new Token[listTokens.size()]), tokens);
+  }
+
+  @Test
+  public void testIsEmptyOperator() throws Exception {
+    QueryLexer lexer = new QueryLexer();
+    Token[] tokens = lexer.tokens("category1.isEmpty()");
+
+    List<Token> listTokens = new ArrayList<Token>();
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".isEmpty("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "category1"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
 
     assertArrayEquals(listTokens.toArray(new Token[listTokens.size()]), tokens);
   }

+ 88 - 1
ambari-server/src/test/java/org/apache/ambari/server/api/predicate/QueryParserTest.java

@@ -114,7 +114,7 @@ public class QueryParserTest {
   }
 
   @Test
-  public void testParse_simpleNotOp() throws Exception {
+  public void testParse_NotOp__simple() throws Exception {
     List<Token> listTokens = new ArrayList<Token>();
     //!a=b
     listTokens.add(new Token(Token.TYPE.LOGICAL_UNARY_OPERATOR, "!"));
@@ -152,6 +152,93 @@ public class QueryParserTest {
     assertEquals(andPred, p);
   }
 
+  @Test
+  public void testParse_InOp__simple() throws Exception {
+    List<Token> listTokens = new ArrayList<Token>();
+    // foo.in(one,two,3)
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".in("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "foo"));
+    listTokens.add(new Token(Token.TYPE.VALUE_OPERAND, "one,two,3"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
+
+    QueryParser parser = new QueryParser();
+    Predicate p = parser.parse(listTokens.toArray(new Token[listTokens.size()]));
+
+    EqualsPredicate ep1 = new EqualsPredicate("foo", "one");
+    EqualsPredicate ep2 = new EqualsPredicate("foo", "two");
+    EqualsPredicate ep3 = new EqualsPredicate("foo", "3");
+
+    OrPredicate orPredicate = new OrPredicate(ep1, ep2, ep3);
+
+    assertEquals(orPredicate, p);
+  }
+
+  @Test
+  public void testParse_InOp__exception() throws Exception {
+    List<Token> listTokens = new ArrayList<Token>();
+    // foo.in()
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".in("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "foo"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
+
+    QueryParser parser = new QueryParser();
+    try {
+      parser.parse(listTokens.toArray(new Token[listTokens.size()]));
+      fail("Expected InvalidQueryException due to missing right operand");
+    } catch (InvalidQueryException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testParse_isEmptyOp__simple() throws Exception {
+    List<Token> listTokens = new ArrayList<Token>();
+    // category1.isEmpty()
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".isEmpty("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "category1"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
+
+    QueryParser parser = new QueryParser();
+    Predicate p = parser.parse(listTokens.toArray(new Token[listTokens.size()]));
+
+    assertEquals(new CategoryIsEmptyPredicate("category1"), p);
+  }
+
+  @Test
+  public void testParse_isEmptyOp__exception() throws Exception {
+    List<Token> listTokens = new ArrayList<Token>();
+    // category1.isEmpty()
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".isEmpty("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "category1"));
+    // missing closing bracket
+
+    QueryParser parser = new QueryParser();
+    try {
+      parser.parse(listTokens.toArray(new Token[listTokens.size()]));
+      fail("Expected InvalidQueryException due to missing closing bracket");
+    } catch (InvalidQueryException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testParse_isEmptyOp__exception2() throws Exception {
+    List<Token> listTokens = new ArrayList<Token>();
+    // category1.isEmpty()
+    listTokens.add(new Token(Token.TYPE.RELATIONAL_OPERATOR_FUNC, ".isEmpty("));
+    listTokens.add(new Token(Token.TYPE.PROPERTY_OPERAND, "category1"));
+    listTokens.add(new Token(Token.TYPE.VALUE_OPERAND, "one,two,3"));
+    listTokens.add(new Token(Token.TYPE.BRACKET_CLOSE, ")"));
+
+    QueryParser parser = new QueryParser();
+    try {
+      parser.parse(listTokens.toArray(new Token[listTokens.size()]));
+      fail("Expected InvalidQueryException due to existence of right operand");
+    } catch (InvalidQueryException e) {
+      // expected
+    }
+  }
+
   @Test
   public void testParse_noTokens() throws InvalidQueryException {
     assertNull(new QueryParser().parse(new Token[0]));