You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2018/03/04 18:00:26 UTC

[1/2] zeppelin git commit: [ZEPPELIN-3194][NEW-INTERPRETER] SAP Universe interpreter

Repository: zeppelin
Updated Branches:
  refs/heads/master bfc93dc03 -> 63c53fcc5


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java
new file mode 100644
index 0000000..dc9099d
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java
@@ -0,0 +1,643 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.*;
+
+/**
+ * Util class for convert request from Zeppelin to SAP
+ */
+public class UniverseUtil {
+
+  private static final String COMPRASION_START_TEMPLATE = "<comparisonFilter path=\"%s\" " +
+      "operator=\"%s\" id=\"%s\">\n";
+  private static final String COMPRASION_END_TEMPLATE = "</comparisonFilter>\n";
+  private static final String COMPARISON_FILTER = "<comparisonFilter id=\"%s\" path=\"%s\" " +
+      "operator=\"%s\"/>\n";
+  private static final String CONST_OPERAND_START_TEMPLATE = "<constantOperand>\n";
+  private static final String CONST_OPERAND_END_TEMPLATE = "</constantOperand>\n";
+  private static final String CONST_OPERAND_VALUE_TEMPLATE = "<value>\n" +
+      "<caption type=\"%s\">%s</caption>\n</value>\n";
+  private static final String PREDEFINED_FILTER_TEMPLATE = "<predefinedFilter path=\"%s\"" +
+      " id=\"%s\"/>\n";
+  private static final String OBJECT_OPERAND_TEMPLATE = "<objectOperand id=\"%s\" path=\"%s\"/>\n";
+  private static final String RESULT_START_TEMPLATE = "<resultObjects>\n";
+  private static final String RESULT_END_TEMPLATE = "</resultObjects>\n";
+  private static final String RESULT_OBJ_TEMPLATE = "<resultObject path=\"%s\" id=\"%s\"/>\n";
+
+  private static final String MARKER_EQUAL = "#EqualTo#";
+  private static final String MARKER_LESS_EQUAL = "#LessThanOrEqualTo#";
+  private static final String MARKER_NOT_EQUAL = "#NotEqualTo#";
+  private static final String MARKER_LESS = "#LessThan#";
+  private static final String MARKER_GREATER_EQUALS = "#GreaterThanOrEqualTo#";
+  private static final String MARKER_GREATER = "#GreaterThan#";
+  private static final String MARKER_IN = "#InList#";
+  private static final String MARKER_NOT_IN = "#NotInList#";
+  private static final String MARKER_NULL = "#IsNull#";
+  private static final String MARKER_NOT_NULL = "#IsNotNull#";
+  private static final String MARKER_FILTER = "#filter#";
+  private static final String MARKER_AND = "#and#";
+  private static final String MARKER_OR = "#or#";
+  private static final String MARKER_BACKSPACE = "#backspace#";
+  private static final String MARKER_LEFT_BRACE = "#left_brace#";
+  private static final String MARKER_RIGHT_BRACE = "#right_brace#";
+
+
+  private static final String LEFT_BRACE = "(";
+  private static final String RIGHT_BRACE = ")";
+
+  public static final Map<String, Integer> OPERATIONS;
+
+  static {
+    OPERATIONS = new HashMap<>();
+    OPERATIONS.put(MARKER_EQUAL, 1);
+    OPERATIONS.put(MARKER_LESS_EQUAL, 1);
+    OPERATIONS.put(MARKER_NOT_EQUAL, 1);
+    OPERATIONS.put(MARKER_LESS, 1);
+    OPERATIONS.put(MARKER_GREATER_EQUALS, 1);
+    OPERATIONS.put(MARKER_GREATER, 1);
+    OPERATIONS.put(MARKER_IN, 1);
+    OPERATIONS.put(MARKER_NOT_IN, 1);
+    OPERATIONS.put(MARKER_NULL, 1);
+    OPERATIONS.put(MARKER_NOT_NULL, 1);
+    OPERATIONS.put(MARKER_FILTER, 1);
+    OPERATIONS.put(MARKER_AND, 2);
+    OPERATIONS.put(MARKER_OR, 3);
+  }
+
+  public UniverseQuery convertQuery(String text, UniverseClient client, String token)
+      throws UniverseException {
+    StringBuilder select = new StringBuilder();
+    StringBuilder universe = new StringBuilder();
+    StringBuilder buf = new StringBuilder();
+    StringBuilder resultObj = new StringBuilder();
+    StringBuilder whereBuf = new StringBuilder();
+    UniverseInfo universeInfo = null;
+    String where = null;
+    boolean singleQuoteClosed = true;
+    boolean pathClosed = true;
+    boolean universePart = false;
+    boolean selectPart = false;
+    boolean wherePart = false;
+    boolean listOperator = false;
+    boolean operatorPosition = false;
+    Map<String, UniverseNodeInfo> nodeInfos = null;
+
+    char[] array = text.toCharArray();
+    for (int i = 0; i < array.length; i++) {
+      char c = array[i];
+      buf.append(c);
+      if (c == '\'') {
+        if (i == 0 || array[i - 1] != '\\') {
+          singleQuoteClosed = !singleQuoteClosed;
+        }
+      }
+      if (c == '[' && pathClosed && singleQuoteClosed) {
+        pathClosed = false;
+        if (wherePart) {
+          operatorPosition = false;
+        }
+      }
+      if (c == ']' && !pathClosed && singleQuoteClosed) {
+        pathClosed = true;
+        if (wherePart) {
+          operatorPosition = true;
+          if (i + 1 == array.length || (array[i + 1] != '.'
+              && isFilter(String.format("%s]", whereBuf.toString()), text.substring(i + 1)))) {
+            whereBuf.append(c);
+            whereBuf.append(MARKER_FILTER);
+            if (i + 1 == array.length) {
+              wherePart = false;
+              where = parseWhere(whereBuf.toString(), nodeInfos);
+            }
+            continue;
+          }
+        }
+      }
+      if (c == '(' && wherePart && pathClosed && singleQuoteClosed) {
+        if (listOperator) {
+          whereBuf.append(MARKER_LEFT_BRACE);
+          continue;
+        } else {
+          whereBuf.append(c);
+          continue;
+        }
+      }
+      if (c == ')' && wherePart && pathClosed && singleQuoteClosed) {
+        if (listOperator) {
+          whereBuf.append(MARKER_RIGHT_BRACE);
+          listOperator = false;
+          continue;
+        } else {
+          whereBuf.append(c);
+          continue;
+        }
+      }
+
+      if (!universePart && singleQuoteClosed
+          && buf.toString().toLowerCase().endsWith("universe")) {
+        universePart = true;
+        continue;
+      }
+
+      if (universePart) {
+        if (c == ';' && singleQuoteClosed) {
+          universePart = false;
+          if (universe.toString().trim().length() > 2) {
+            String universeName =
+                universe.toString().trim().substring(1, universe.toString().trim().length() - 1);
+            universeInfo = client.getUniverseInfo(universeName);
+            nodeInfos = client.getUniverseNodesInfo(token, universeName);
+          }
+        } else {
+          universe.append(c);
+        }
+        continue;
+      }
+
+      if (!selectPart && pathClosed && singleQuoteClosed
+          && buf.toString().toLowerCase().endsWith("select")) {
+        if (StringUtils.isBlank(universe.toString())) {
+          throw new UniverseException("Not found universe name");
+        }
+        selectPart = true;
+        select.append(RESULT_START_TEMPLATE);
+        continue;
+      }
+
+      if (!wherePart && pathClosed && singleQuoteClosed) {
+        if (buf.toString().toLowerCase().endsWith("where")) {
+          wherePart = true;
+        }
+        if (buf.toString().toLowerCase().endsWith("where") || i == array.length - 1) {
+          selectPart = false;
+          select.append(parseResultObj(resultObj.toString().replaceAll("(?i)wher$", ""), nodeInfos));
+          select.append(RESULT_END_TEMPLATE);
+          continue;
+        }
+      }
+
+      if (selectPart) {
+        if (pathClosed && singleQuoteClosed && c == ',') {
+          select.append(parseResultObj(resultObj.toString(), nodeInfos));
+          resultObj = new StringBuilder();
+        } else {
+          resultObj.append(c);
+        }
+        continue;
+      }
+
+      if (wherePart) {
+        if (c == ';' && pathClosed && singleQuoteClosed) {
+          wherePart = false;
+          where = parseWhere(whereBuf.toString(), nodeInfos);
+        } else {
+          if (!singleQuoteClosed || !pathClosed) {
+            switch (c) {
+              case ' ':
+              case '\n':
+                whereBuf.append(MARKER_BACKSPACE);
+                break;
+              case '(':
+                whereBuf.append(MARKER_LEFT_BRACE);
+                break;
+              case ')':
+                whereBuf.append(MARKER_RIGHT_BRACE);
+                break;
+              default:
+                whereBuf.append(c);
+            }
+          } else if (pathClosed) {
+            if ((c == 'a' || c == 'A') && i < array.length - 2 &&
+                text.substring(i, i + 3).equalsIgnoreCase("and")) {
+              i += 2;
+              whereBuf.append(MARKER_AND);
+              operatorPosition = false;
+              continue;
+            }
+            if ((c == 'o' || c == 'O') && i < array.length - 1 &&
+                text.substring(i, i + 2).equalsIgnoreCase("or")) {
+              i += 1;
+              whereBuf.append(MARKER_OR);
+              operatorPosition = false;
+              continue;
+            }
+            if (operatorPosition) {
+              switch (c) {
+                case '=':
+                  whereBuf.append(MARKER_EQUAL);
+                  operatorPosition = false;
+                  break;
+                case '<':
+                  if (i + 1 < array.length) {
+                    if (array[i + 1] == '=') {
+                      whereBuf.append(MARKER_LESS_EQUAL);
+                      operatorPosition = false;
+                      i++;
+                      break;
+                    } else if (array[i + 1] == '>') {
+                      whereBuf.append(MARKER_NOT_EQUAL);
+                      operatorPosition = false;
+                      i++;
+                      break;
+                    }
+                  }
+                  operatorPosition = false;
+                  whereBuf.append(MARKER_LESS);
+                  break;
+                case '>':
+                  if (i + 1 < array.length) {
+                    if (array[i + 1] == '=') {
+                      whereBuf.append(MARKER_GREATER_EQUALS);
+                      operatorPosition = false;
+                      i++;
+                      break;
+                    }
+                  }
+                  operatorPosition = false;
+                  whereBuf.append(MARKER_GREATER);
+                  break;
+                case 'i':
+                case 'I':
+                  boolean whileI = true;
+                  StringBuilder operI = new StringBuilder();
+                  operI.append(c);
+                  while (whileI) {
+                    i++;
+                    if (i >= array.length) {
+                      whileI = false;
+                    }
+
+                    if (array[i] != ' ' && array[i] != '\n') {
+                      operI.append(array[i]);
+                    } else {
+                      continue;
+                    }
+                    String tmp = operI.toString().toLowerCase();
+                    if (tmp.equals("in")) {
+                      whereBuf.append(MARKER_IN);
+                      listOperator = true;
+                      whileI = false;
+                      operatorPosition = false;
+                    } else if (tmp.equals("isnull")) {
+                      whereBuf.append(MARKER_NULL);
+                      whileI = false;
+                      operatorPosition = false;
+                    } else if (tmp.equals("isnotnull")) {
+                      whereBuf.append(MARKER_NOT_NULL);
+                      whileI = false;
+                      operatorPosition = false;
+                    }
+                    // longest 9 - isnotnull
+                    if (tmp.length() > 8) {
+                      whileI = false;
+                    }
+                  }
+                  break;
+                case 'n':
+                case 'N':
+                  boolean whileN = true;
+                  StringBuilder operN = new StringBuilder();
+                  operN.append(c);
+                  while (whileN) {
+                    i++;
+                    if (i >= array.length) {
+                      whileN = false;
+                    }
+
+                    if (array[i] != ' ' && array[i] != '\n') {
+                      operN.append(array[i]);
+                    } else {
+                      continue;
+                    }
+
+                    String tmp = operN.toString().toLowerCase();
+
+                    if (tmp.equals("notin")) {
+                      whereBuf.append(MARKER_NOT_IN);
+                      listOperator = true;
+                      whileN = false;
+                      operatorPosition = false;
+                    }
+
+                    // longest 5 - notin
+                    if (tmp.length() > 4) {
+                      whileN = false;
+                    }
+                  }
+                  break;
+                default:
+                  whereBuf.append(c);
+              }
+            } else {
+              whereBuf.append(c);
+            }
+          } else {
+            whereBuf.append(c);
+          }
+        }
+      }
+    }
+
+    if (wherePart && StringUtils.isBlank(where)) {
+      throw new UniverseException("Incorrect block where");
+    }
+
+    UniverseQuery universeQuery = new UniverseQuery(select.toString().trim(),
+        where, universeInfo);
+
+    if (!universeQuery.isCorrect()) {
+      throw new UniverseException("Incorrect query");
+    }
+
+    return universeQuery;
+  }
+
+  private String parseWhere(String where, Map<String, UniverseNodeInfo> nodeInfos)
+      throws UniverseException {
+    List<String> out = new ArrayList<>();
+    Stack<String> stack = new Stack<>();
+
+    where = where.replaceAll("\\s*", "");
+
+    Set<String> operationSymbols = new HashSet<>(OPERATIONS.keySet());
+    operationSymbols.add(LEFT_BRACE);
+    operationSymbols.add(RIGHT_BRACE);
+
+    int index = 0;
+
+    boolean findNext = true;
+    while (findNext) {
+      int nextOperationIndex = where.length();
+      String nextOperation = "";
+      for (String operation : operationSymbols) {
+        int i = where.indexOf(operation, index);
+        if (i >= 0 && i < nextOperationIndex) {
+          nextOperation = operation;
+          nextOperationIndex = i;
+        }
+      }
+      if (nextOperationIndex == where.length()) {
+        findNext = false;
+      } else {
+        if (index != nextOperationIndex) {
+          out.add(where.substring(index, nextOperationIndex));
+        }
+        if (nextOperation.equals(LEFT_BRACE)) {
+          stack.push(nextOperation);
+        }
+        else if (nextOperation.equals(RIGHT_BRACE)) {
+          while (!stack.peek().equals(LEFT_BRACE)) {
+            out.add(stack.pop());
+            if (stack.empty()) {
+              throw new UniverseException("Unmatched brackets");
+            }
+          }
+          stack.pop();
+        }
+        else {
+          while (!stack.empty() && !stack.peek().equals(LEFT_BRACE) &&
+              (OPERATIONS.get(nextOperation) >= OPERATIONS.get(stack.peek()))) {
+            out.add(stack.pop());
+          }
+          stack.push(nextOperation);
+        }
+        index = nextOperationIndex + nextOperation.length();
+      }
+    }
+    if (index != where.length()) {
+      out.add(where.substring(index));
+    }
+    while (!stack.empty()) {
+      out.add(stack.pop());
+    }
+    StringBuffer result = new StringBuffer();
+    if (!out.isEmpty())
+      result.append(out.remove(0));
+    while (!out.isEmpty())
+      result.append(" ").append(out.remove(0));
+
+    // result contains the reverse polish notation
+    return convertWhereToXml(result.toString(), nodeInfos);
+  }
+
+  private String parseResultObj(String resultObj, Map<String, UniverseNodeInfo> nodeInfos)
+      throws UniverseException {
+    if (StringUtils.isNotBlank(resultObj)) {
+      UniverseNodeInfo nodeInfo = nodeInfos.get(resultObj.trim());
+      if (nodeInfo != null) {
+        return String.format(RESULT_OBJ_TEMPLATE, nodeInfo.getNodePath(), nodeInfo.getId());
+      }
+      throw new UniverseException(String.format("Not found information about: \"%s\"",
+          resultObj.trim()));
+    }
+
+    return StringUtils.EMPTY;
+  }
+
+  private String convertWhereToXml(String rpn, Map<String, UniverseNodeInfo> nodeInfos)
+      throws UniverseException {
+    StringTokenizer tokenizer = new StringTokenizer(rpn, " ");
+
+    Stack<String> stack = new Stack();
+
+    while (tokenizer.hasMoreTokens()) {
+      StringBuilder tmp = new StringBuilder();
+      String token = tokenizer.nextToken();
+      if (!OPERATIONS.keySet().contains(token)) {
+        stack.push(token.trim());
+      } else {
+        String rightOperand = revertReplace(stack.pop());
+        String operator = token.replaceAll("^#|#$", "");
+
+        if (token.equalsIgnoreCase(MARKER_NOT_NULL) || token.equalsIgnoreCase(MARKER_NULL)) {
+          UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand);
+          stack.push(String.format(COMPARISON_FILTER, rightOperandInfo.getId(),
+              rightOperandInfo.getNodePath(), operator));
+          continue;
+        }
+
+        if (token.equalsIgnoreCase(MARKER_FILTER)) {
+          UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand);
+          stack.push(String.format(PREDEFINED_FILTER_TEMPLATE, rightOperandInfo.getNodePath(),
+              rightOperandInfo.getId()));
+          continue;
+        }
+
+        String leftOperand = stack.empty() ? null : revertReplace(stack.pop());
+
+        if (token.equalsIgnoreCase(MARKER_AND) || token.equalsIgnoreCase(MARKER_OR)) {
+          if (rightOperand.matches("^\\[.*\\]$")) {
+            UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand);
+            if (rightOperandInfo == null) {
+              throw new UniverseException(String.format("Not found information about: \"%s\"",
+                  rightOperand));
+            }
+            rightOperand = String.format(PREDEFINED_FILTER_TEMPLATE,
+                rightOperandInfo.getNodePath(), rightOperandInfo.getId());
+          }
+          if (leftOperand.matches("^\\[.*\\]$")) {
+            UniverseNodeInfo leftOperandInfo = nodeInfos.get(leftOperand);
+            if (leftOperandInfo == null) {
+              throw new UniverseException(String.format("Not found information about: \"%s\"",
+                  leftOperand));
+            }
+            leftOperand = String.format(PREDEFINED_FILTER_TEMPLATE, leftOperandInfo.getNodePath(),
+                leftOperandInfo.getId());
+          }
+          tmp.append(String.format("<%s>\n", operator));
+          tmp.append(leftOperand);
+          tmp.append("\n");
+          tmp.append(rightOperand);
+          tmp.append("\n");
+          tmp.append(String.format("</%s>\n", operator));
+          stack.push(tmp.toString());
+          continue;
+        }
+
+        UniverseNodeInfo leftOperandInfo = nodeInfos.get(leftOperand);
+        if (leftOperandInfo == null) {
+          throw new UniverseException(String.format("Not found information about: \"%s\"",
+              leftOperand));
+        }
+        if (token.equalsIgnoreCase(MARKER_IN) || token.equalsIgnoreCase(MARKER_NOT_IN)) {
+          String listValues = rightOperand.replaceAll("^\\(|\\)$", "").trim();
+          boolean startItem = false;
+          List<String> values = new ArrayList<>();
+          StringBuilder value = new StringBuilder();
+          boolean isNumericList = false;
+          if (listValues.charAt(0) != '\'') {
+            isNumericList = true;
+          }
+          if (isNumericList) {
+            String[] nums = listValues.split(",");
+            for (String num : nums) {
+              values.add(num.trim());
+            }
+          } else {
+            for (int i = 0; i < listValues.length(); i++) {
+              char c = listValues.charAt(i);
+              if (c == '\'' && (i == 0 || listValues.charAt(i - 1) != '\\')) {
+                startItem = !startItem;
+                if (!startItem) {
+                  values.add(value.toString());
+                  value = new StringBuilder();
+                }
+                continue;
+              }
+              if (startItem) {
+                value.append(c);
+              }
+            }
+          }
+
+          if (!values.isEmpty()) {
+            tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(),
+                operator, leftOperandInfo.getId()));
+            tmp.append(CONST_OPERAND_START_TEMPLATE);
+            String type = isNumericList ? "Numeric" : "String";
+            for (String v : values) {
+              tmp.append(String.format(CONST_OPERAND_VALUE_TEMPLATE, type, v));
+            }
+            tmp.append(CONST_OPERAND_END_TEMPLATE);
+            tmp.append(COMPRASION_END_TEMPLATE);
+            stack.push(tmp.toString());
+          }
+          continue;
+        }
+
+        // EqualTo, LessThanOrEqualTo, NotEqualTo, LessThan, GreaterThanOrEqualTo, GreaterThan
+        UniverseNodeInfo rightOperandInfo = null;
+        if (rightOperand.startsWith("[") && rightOperand.endsWith("]")) {
+          rightOperandInfo = nodeInfos.get(rightOperand);
+          if (rightOperandInfo == null) {
+            throw new UniverseException(String.format("Not found information about: \"%s\"",
+                rightOperand));
+          }
+        }
+        if (OPERATIONS.containsKey(token)) {
+          if (rightOperandInfo != null) {
+            tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(),
+                operator, leftOperandInfo.getId()));
+            tmp.append(String.format(OBJECT_OPERAND_TEMPLATE, rightOperandInfo.getId(),
+                rightOperandInfo.getNodePath()));
+            tmp.append(COMPRASION_END_TEMPLATE);
+          } else {
+            String type = rightOperand.startsWith("'") ? "String" : "Numeric";
+            String value = rightOperand.replaceAll("^'|'$", "");
+            tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(),
+                operator, leftOperandInfo.getId()));
+            tmp.append(CONST_OPERAND_START_TEMPLATE);
+            tmp.append(String.format(CONST_OPERAND_VALUE_TEMPLATE, type, value));
+            tmp.append(CONST_OPERAND_END_TEMPLATE);
+            tmp.append(COMPRASION_END_TEMPLATE);
+          }
+          stack.push(tmp.toString());
+          continue;
+        }
+        throw new UniverseException(String.format("Incorrect syntax after: \"%s\"", leftOperand));
+      }
+    }
+
+    return stack.pop();
+  }
+
+  private String revertReplace(String s) {
+    return s.replaceAll(MARKER_BACKSPACE, " ")
+        .replaceAll(MARKER_LEFT_BRACE, "(")
+        .replaceAll(MARKER_RIGHT_BRACE, ")");
+  }
+
+  private boolean isFilter(String buf, String after) {
+    boolean result = false;
+    String[] parts = buf.trim().split("\\s");
+    if (parts[parts.length - 1].matches("^\\[.*\\]$")) {
+      // check before
+      if (parts.length == 1) {
+        result = true;
+      } else {
+        int count = parts.length - 2;
+        Set<String> operations = new HashSet(OPERATIONS.keySet());
+        operations.remove(MARKER_AND);
+        operations.remove(MARKER_OR);
+        while (count >= 0) {
+          String p = parts[count];
+          if (StringUtils.isNotBlank(p)) {
+            if (!operations.contains(p)) {
+              result = true;
+              break;
+            } else {
+              return false;
+            }
+          }
+          count--;
+        }
+      }
+      after = after.trim();
+      // check after
+      if (result && !after.startsWith("and") && !after.startsWith("or") &&
+          !after.startsWith(";") && StringUtils.isNotBlank(after)) {
+        result = false;
+      }
+    }
+
+    return result;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/resources/interpreter-setting.json
----------------------------------------------------------------------
diff --git a/sap/src/main/resources/interpreter-setting.json b/sap/src/main/resources/interpreter-setting.json
new file mode 100644
index 0000000..cb5cf94
--- /dev/null
+++ b/sap/src/main/resources/interpreter-setting.json
@@ -0,0 +1,42 @@
+[
+  {
+    "group": "sap",
+    "name": "universe",
+    "className": "org.apache.zeppelin.sap.UniverseInterpreter",
+    "defaultInterpreter": true,
+    "properties": {
+      "universe.api.url": {
+        "envName": null,
+        "propertyName": "universe.api.url",
+        "defaultValue": "http://localhost:6405/biprws",
+        "description": "API url of Universe",
+        "type": "url"
+      },
+      "universe.user": {
+        "envName": null,
+        "propertyName": "universe.user",
+        "defaultValue": "",
+        "description": "Username for API of Universe",
+        "type": "string"
+      },
+      "universe.password": {
+        "envName": null,
+        "propertyName": "universe.password",
+        "defaultValue": "",
+        "description": "Password for API of Universe",
+        "type": "password"
+      },
+      "universe.authType": {
+        "envName": null,
+        "propertyName": "universe.password",
+        "defaultValue": "secEnterprise",
+        "description": "Type of authentication for API of Universe. Available values: secEnterprise, secLDAP, secWinAD, secSAPR3",
+        "type": "string"
+      }
+    },
+    "editor": {
+      "editOnDblClick": false,
+      "completionKey": "TAB"
+    }
+  }
+]

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/resources/universe.keywords
----------------------------------------------------------------------
diff --git a/sap/src/main/resources/universe.keywords b/sap/src/main/resources/universe.keywords
new file mode 100644
index 0000000..0811bfb
--- /dev/null
+++ b/sap/src/main/resources/universe.keywords
@@ -0,0 +1 @@
+universe,select,where,and,or,is null,is not null,in
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java
----------------------------------------------------------------------
diff --git a/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java
new file mode 100644
index 0000000..91a4217
--- /dev/null
+++ b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java
@@ -0,0 +1,134 @@
+/**
+ * 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.zeppelin.sap.universe;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.zeppelin.completer.CachedCompleter;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Universe completer unit tests
+ */
+public class UniverseCompleterTest {
+
+  private UniverseCompleter universeCompleter;
+  private UniverseUtil universeUtil;
+  private UniverseClient universeClient;
+
+  @Before
+  public void beforeTest() throws UniverseException {
+    universeCompleter = new UniverseCompleter(0);
+    universeUtil = new UniverseUtil();
+    Map<String, UniverseInfo> universes = new HashMap<>();
+
+    universes.put("testUniverse", new UniverseInfo("1", "testUniverse", "uvx"));
+    universes.put("test with space", new UniverseInfo("2", "test with space", "uvx"));
+    universes.put("(GLOBAL) universe", new UniverseInfo("3", "(GLOBAL) universe", "uvx"));
+    UniverseInfo universeInfo = new UniverseInfo("1", "testUniverse", "uvx");
+    Map<String, UniverseNodeInfo> testUniverseNodes = new HashMap<>();
+    testUniverseNodes.put("[Dimension].[Test].[name1]",
+        new UniverseNodeInfo("name1id", "name1", "dimension", "Dimension\\Test",
+            "Dimension|folder\\Test|folder\\name1|dimension"));
+    testUniverseNodes.put("[Dimension].[Test].[name2]",
+        new UniverseNodeInfo("name2id", "name2", "dimension", "Dimension\\Test",
+            "Dimension|folder\\Test|folder\\name2|dimension"));
+    testUniverseNodes.put("[Filter].[name3]",
+        new UniverseNodeInfo("name3id", "name3", "filter", "Filter",
+            "Filter|folder\\name3|filter"));
+    testUniverseNodes.put("[Filter].[name4]",
+        new UniverseNodeInfo("name4id", "name4", "filter", "Filter",
+            "Filter|folder\\name4|filter"));
+    testUniverseNodes.put("[Measure].[name5]",
+        new UniverseNodeInfo("name5id", "name5", "measure", "Measure",
+            "Measure|folder\\name5|measure"));
+
+    universeClient = mock(UniverseClient.class);
+    when(universeClient.getUniverseInfo(anyString())).thenReturn(universeInfo);
+    when(universeClient.getUniverseNodesInfo(anyString(), anyString()))
+        .thenReturn(testUniverseNodes);
+    when(universeClient.getUniversesMap()).thenReturn(universes);
+  }
+
+  @Test
+  public void testCreateUniverseNameCompleter() {
+    String buffer = "universe [";
+    List<CharSequence> candidates = new ArrayList<>();
+    universeCompleter.createOrUpdate(universeClient, null, buffer, 9);
+    CachedCompleter completer = universeCompleter.getUniverseCompleter();
+    assertNull(completer);
+    universeCompleter.createOrUpdate(universeClient, null, buffer, 10);
+    completer = universeCompleter.getUniverseCompleter();
+    assertNotNull(completer);
+
+    completer.getCompleter().complete(StringUtils.EMPTY, 0, candidates);
+    assertEquals(3, candidates.size());
+  }
+
+  @Test
+  public void testCreateUniverseNodesCompleter() {
+    String buffer = "universe [testUniverse]; select [";
+    List<CharSequence> candidates = new ArrayList<>();
+    universeCompleter.createOrUpdate(universeClient, null, buffer, 32);
+    Map<String, CachedCompleter> completerMap = universeCompleter.getUniverseInfoCompletersMap();
+    assertFalse(completerMap.containsKey("testUniverse"));
+    universeCompleter.createOrUpdate(universeClient, null, buffer, 33);
+    completerMap = universeCompleter.getUniverseInfoCompletersMap();
+    assertTrue(completerMap.containsKey("testUniverse"));
+    CachedCompleter completer = completerMap.get("testUniverse");
+
+    completer.getCompleter().complete(StringUtils.EMPTY, 0, candidates);
+    assertEquals(3, candidates.size());
+    List<String> candidatesStrings = new ArrayList<>();
+    for (Object o : candidates) {
+      UniverseNodeInfo info = (UniverseNodeInfo) o;
+      candidatesStrings.add(info.getName());
+    }
+    List<String> expected = Arrays.asList("Filter", "Measure", "Dimension");
+    Collections.sort(candidatesStrings);
+    Collections.sort(expected);
+    assertEquals(expected, candidatesStrings);
+  }
+
+  @Test
+  public void testNestedUniverseNodes() {
+    String buffer = "universe [testUniverse]; select [Dimension].[Test].[n";
+    List<CharSequence> candidates = new ArrayList<>();
+
+    universeCompleter.createOrUpdate(universeClient, null, buffer, 53);
+    Map<String, CachedCompleter> completerMap = universeCompleter.getUniverseInfoCompletersMap();
+    assertTrue(completerMap.containsKey("testUniverse"));
+    CachedCompleter completer = completerMap.get("testUniverse");
+
+    completer.getCompleter().complete("[Dimension].[Test].[n", 21, candidates);
+    assertEquals(2, candidates.size());
+    List<String> candidatesStrings = new ArrayList<>();
+    for (Object o : candidates) {
+      UniverseNodeInfo info = (UniverseNodeInfo) o;
+      candidatesStrings.add(info.getName());
+    }
+    List<String> expected = Arrays.asList("name1", "name2");
+    Collections.sort(candidatesStrings);
+    Collections.sort(expected);
+    assertEquals(expected, candidatesStrings);
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java
----------------------------------------------------------------------
diff --git a/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java
new file mode 100644
index 0000000..81a027e
--- /dev/null
+++ b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UniverseUtilTest {
+
+  private UniverseClient universeClient;
+  private UniverseUtil universeUtil;
+
+  @Before
+  public void beforeTest() throws UniverseException {
+      universeUtil = new UniverseUtil();
+      UniverseInfo universeInfo = new UniverseInfo("1", "testUniverse", "uvx");
+      Map<String, UniverseNodeInfo> testUniverseNodes = new HashMap<>();
+      testUniverseNodes.put("[Dimension].[Test].[name1]",
+          new UniverseNodeInfo("name1id", "name1", "dimension", "Dimension\\Test",
+              "Dimension|folder\\Test|folder\\name1|dimension"));
+      testUniverseNodes.put("[Dimension].[Test].[name2]",
+          new UniverseNodeInfo("name2id", "name2", "dimension", "Filter\\Test",
+              "Dimension|folder\\Test|folder\\name2|dimension"));
+      testUniverseNodes.put("[Filter].[name3]",
+          new UniverseNodeInfo("name3id", "name3", "filter", "Filter",
+              "Filter|folder\\name3|filter"));
+      testUniverseNodes.put("[Filter].[name4]",
+          new UniverseNodeInfo("name4id", "name4", "filter", "Filter",
+              "Filter|folder\\name4|filter"));
+      testUniverseNodes.put("[Measure].[name5]",
+          new UniverseNodeInfo("name5id", "name5", "measure", "Measure",
+              "Measure|folder\\name5|measure"));
+
+      universeClient = mock(UniverseClient.class);
+      when(universeClient.getUniverseInfo(anyString())).thenReturn(universeInfo);
+      when(universeClient.getUniverseNodesInfo(anyString(), anyString()))
+          .thenReturn(testUniverseNodes);
+  }
+
+  @Test
+  public void testForConvert() throws UniverseException {
+      String request = "universe [testUniverse];\n" +
+          "select [Measure].[name5]\n" +
+          "where [Filter].[name3] and [Dimension].[Test].[name2] > 1;";
+      UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null);
+      assertNotNull(universeQuery);
+      assertNotNull(universeQuery.getUniverseInfo());
+      assertEquals("<resultObjects>\n" +
+					"<resultObject path=\"Measure|folder\\name5|measure\" id=\"name5id\"/>\n" +
+					"</resultObjects>", universeQuery.getSelect());
+      assertEquals("<and>\n" +
+          "<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n" +
+          "\n<comparisonFilter path=\"Dimension|folder\\Test|folder\\name2|dimension\"" +
+          " operator=\"GreaterThan\" id=\"name2id\">\n" +
+          "<constantOperand>\n" +
+          "<value>\n" +
+          "<caption type=\"Numeric\">1</caption>\n" +
+          "</value>\n" +
+          "</constantOperand>\n" +
+          "</comparisonFilter>\n\n" +
+          "</and>\n", universeQuery.getWhere());
+      assertEquals("testUniverse", universeQuery.getUniverseInfo().getName());
+  }
+
+  @Test
+  public void testConvertConditions() throws UniverseException {
+    String request = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name3] " +
+        "and [Dimension].[Test].[name2] >= 1 " +
+        "and [Dimension].[Test].[name2] < 20 " +
+        "and [Dimension].[Test].[name1] <> 'test' " +
+        "and [Dimension].[Test].[name1] is not null " +
+        "and [Measure].[name5] is null" +
+        "and [Dimension].[Test].[name1] in ('var1', 'v a r 2') " +
+        "and [Dimension].[Test].[name1] in ('var1','withoutspaces')" +
+        "and [Dimension].[Test].[name1] in ('one value')" +
+        "and [Dimension].[Test].[name2] in (1,3,4);";
+    UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null);
+    assertNotNull(universeQuery);
+    assertEquals("<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<and>\n" +
+            "<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name2|dimension\"" +
+            " operator=\"GreaterThanOrEqualTo\" id=\"name2id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">1</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name2|dimension\"" +
+            " operator=\"LessThan\" id=\"name2id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">20</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name1|dimension\"" +
+            " operator=\"NotEqualTo\" id=\"name1id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">test</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter id=\"name1id\" path=\"Dimension|folder\\Test|folder\\name1|dimension\"" +
+            " operator=\"IsNotNull\"/>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter id=\"name5id\" path=\"Measure|folder\\name5|measure\" operator=\"IsNull\"/>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name1|dimension\"" +
+            " operator=\"InList\" id=\"name1id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">var1</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">v a r 2</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name1|dimension\"" +
+            " operator=\"InList\" id=\"name1id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">var1</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">withoutspaces</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name1|dimension\"" +
+            " operator=\"InList\" id=\"name1id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">one value</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name2|dimension\"" +
+            " operator=\"InList\" id=\"name2id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">1</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">3</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">4</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</and>\n",
+        universeQuery.getWhere());
+  }
+
+  @Test(expected = UniverseException.class)
+  public void testFailConvertWithoutUniverse() throws UniverseException {
+    String request = "universe ;\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name3] and [Dimension].[Test].[name2] > 1;";
+    universeUtil.convertQuery(request, universeClient, null);
+  }
+
+  @Test(expected = UniverseException.class)
+  public void testFailConvertWithIncorrectSelect() throws UniverseException {
+    String request = "universe [testUniverse];\n" +
+        "select [not].[exist];";
+    universeUtil.convertQuery(request, universeClient, null);
+  }
+
+
+  @Test(expected = UniverseException.class)
+  public void testFailConvertWithIncorrectCondition() throws UniverseException {
+    String request = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name;";
+    universeUtil.convertQuery(request, universeClient, null);
+  }
+
+  @Test
+  public void testFiltersConditions() throws UniverseException {
+    String request1 = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name3];";
+    String request2 = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Measure].[name5] > 2 and [Filter].[name3];";
+    String request3 = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name3] or [Measure].[name5];";
+    String request4 = "universe [testUniverse];\n" +
+        "select [Measure].[name5]\n" +
+        "where [Filter].[name3] and [Measure].[name5] is null;";
+    UniverseQuery universeQuery = universeUtil.convertQuery(request1, universeClient, null);
+    assertEquals("<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n",
+        universeQuery.getWhere());
+    universeQuery = universeUtil.convertQuery(request2, universeClient, null);
+    assertEquals("<and>\n" +
+            "<comparisonFilter path=\"Measure|folder\\name5|measure\" operator=\"GreaterThan\" id=\"name5id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">2</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n\n" +
+            "</and>\n",
+        universeQuery.getWhere());
+    universeQuery = universeUtil.convertQuery(request3, universeClient, null);
+    assertEquals("<or>\n" +
+            "<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n\n" +
+            "<predefinedFilter path=\"Measure|folder\\name5|measure\" id=\"name5id\"/>\n\n" +
+            "</or>\n",
+        universeQuery.getWhere());
+    universeQuery = universeUtil.convertQuery(request4, universeClient, null);
+    assertEquals("<and>\n" +
+            "<predefinedFilter path=\"Filter|folder\\name3|filter\" id=\"name3id\"/>\n\n" +
+            "<comparisonFilter id=\"name5id\" path=\"Measure|folder\\name5|measure\" operator=\"IsNull\"/>\n\n" +
+            "</and>\n",
+        universeQuery.getWhere());
+  }
+
+  @Test
+  public void testNestedConditions() throws UniverseException {
+    String request = "universe [testUniverse];\n" +
+        "select [Dimension].[Test].[name2]\n" +
+        "where ([Measure].[name5] = 'text' or ([Dimension].[Test].[name1] in ('1','2', '3') and\n" +
+        "[Dimension].[Test].[name2] is not null)) and ([Filter].[name4] or [Measure].[name5] >=12)\n" +
+        "or [Dimension].[Test].[name2] not in (31, 65, 77);";
+    UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null);
+    assertEquals("<or>\n" +
+            "<and>\n" +
+            "<or>\n" +
+            "<comparisonFilter path=\"Measure|folder\\name5|measure\" operator=\"EqualTo\" id=\"name5id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">text</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "<and>\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name1|dimension\" operator=\"InList\" id=\"name1id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">1</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">2</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"String\">3</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "<comparisonFilter id=\"name2id\" path=\"Dimension|folder\\Test|folder\\name2|dimension\" operator=\"IsNotNull\"/>\n\n" +
+            "</and>\n\n" +
+            "</or>\n\n" +
+            "<or>\n" +
+            "<predefinedFilter path=\"Filter|folder\\name4|filter\" id=\"name4id\"/>\n\n" +
+            "<comparisonFilter path=\"Measure|folder\\name5|measure\" operator=\"GreaterThanOrEqualTo\" id=\"name5id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">12</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</or>\n\n" +
+            "</and>\n\n" +
+            "<comparisonFilter path=\"Dimension|folder\\Test|folder\\name2|dimension\" operator=\"NotInList\" id=\"name2id\">\n" +
+            "<constantOperand>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">31</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">65</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">77</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "</or>\n",
+        universeQuery.getWhere());
+  }
+
+  @Test
+  public void testWithoutConditions() throws UniverseException {
+    String request = "universe [testUniverse];\n" +
+        "select [Dimension].[Test].[name2], [Measure].[name5],\n" +
+        "[Dimension].[Test].[name1] ;";
+    UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null);
+    assertNull(universeQuery.getWhere());
+    assertEquals("<resultObjects>\n" +
+            "<resultObject path=\"Dimension|folder\\Test|folder\\name2|dimension\" id=\"name2id\"/>\n" +
+            "<resultObject path=\"Measure|folder\\name5|measure\" id=\"name5id\"/>\n" +
+            "<resultObject path=\"Dimension|folder\\Test|folder\\name1|dimension\" id=\"name1id\"/>\n" +
+            "</resultObjects>",
+        universeQuery.getSelect());
+  }
+
+  @Test
+  public void testCaseSensitive() throws UniverseException {
+    String request = "uniVersE [testUniverse];\n" +
+        "seLEct [Dimension].[Test].[name2], [Measure].[name5]\n" +
+        "whERE [Dimension].[Test].[name2] Is NULl Or [Measure].[name5] IN (1,2) aNd [Measure].[name5] is NOT nUll;";
+    UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null);
+    assertEquals("<resultObjects>\n" +
+            "<resultObject path=\"Dimension|folder\\Test|folder\\name2|dimension\" id=\"name2id\"/>\n" +
+            "<resultObject path=\"Measure|folder\\name5|measure\" id=\"name5id\"/>\n" +
+            "</resultObjects>",
+        universeQuery.getSelect());
+    assertEquals("<or>\n" +
+            "<comparisonFilter id=\"name2id\" path=\"Dimension|folder\\Test|folder\\name2|dimension\" operator=\"IsNull\"/>\n\n" +
+            "<and>\n" +
+            "<comparisonFilter path=\"Measure|folder\\name5|measure\" operator=\"InList\" id=\"name5id\">\n" +
+            "<constantOperand>\n" + "<value>\n" + "<caption type=\"Numeric\">1</caption>\n" +
+            "</value>\n" +
+            "<value>\n" +
+            "<caption type=\"Numeric\">2</caption>\n" +
+            "</value>\n" +
+            "</constantOperand>\n" +
+            "</comparisonFilter>\n\n" +
+            "<comparisonFilter id=\"name5id\" path=\"Measure|folder\\name5|measure\" operator=\"IsNotNull\"/>\n\n" +
+            "</and>\n\n" +
+            "</or>\n",
+        universeQuery.getWhere());
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java
index 20cceda..fc5f380 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java
@@ -24,5 +24,6 @@ public enum CompletionType {
   setting,
   command,
   keyword,
-  path
+  path,
+  universe
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
index 81f9341..bae0c38 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
@@ -692,7 +692,8 @@ public class ZeppelinConfiguration extends XMLConfiguration {
         + "org.apache.zeppelin.beam.BeamInterpreter,"
         + "org.apache.zeppelin.scio.ScioInterpreter,"
         + "org.apache.zeppelin.groovy.GroovyInterpreter,"
-        + "org.apache.zeppelin.neo4j.Neo4jCypherInterpreter"
+        + "org.apache.zeppelin.neo4j.Neo4jCypherInterpreter,"
+        + "org.apache.zeppelin.sap.UniverseInterpreter"
         ),
     ZEPPELIN_INTERPRETER_JSON("zeppelin.interpreter.setting", "interpreter-setting.json"),
     ZEPPELIN_INTERPRETER_DIR("zeppelin.interpreter.dir", "interpreter"),

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
index 971257c..ee17130 100644
--- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
@@ -800,7 +800,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
                     name: v.name,
                     value: v.value,
                     meta: v.meta,
-                    caption: computeCaption(v.value, v.meta),
+                    caption: computeCaption(v.name, v.meta),
                     score: 300,
                   });
                 }


[2/2] zeppelin git commit: [ZEPPELIN-3194][NEW-INTERPRETER] SAP Universe interpreter

Posted by mo...@apache.org.
[ZEPPELIN-3194][NEW-INTERPRETER] SAP Universe interpreter

### What is this PR for?
New interpreter, SAP BusinessObjects (Universes). See [documentation](https://github.com/masyan/zeppelin/blob/9178d1cc80253b9ae58b988443b619127f42d945/docs/interpreter/sap.md)

### What type of PR is it?
[Feature]

### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-3194

### How should this be tested?
see screenshots

### Screenshots (if appropriate)
Autocomplete + get results
![comletion_and_results](https://user-images.githubusercontent.com/705100/35770106-1fb3e9e6-0937-11e8-9ee4-df0c06a7b6c3.gif)

Errors
![errors](https://user-images.githubusercontent.com/705100/35770108-23de9674-0937-11e8-89e1-a0d3380e88bb.gif)

### Questions:
* Does the licenses files need update? no
* Is there breaking changes for older versions? no
* Does this needs documentation? no

Author: Maksim Reshetov <jm...@gmail.com>

Closes #2763 from masyan/ZEPPELIN-3194 and squashes the following commits:

696b7dd54 [Maksim Reshetov] Merge remote-tracking branch 'upstream/master' into ZEPPELIN-3194
1e25fd1ee [Maksim Reshetov] [ZEPPELIN-3194] refactor parser os query. add tests
24336a855 [Maksim Reshetov] [ZEPPELIN-3194] fix parser for result objects and conditions
9178d1cc8 [Maksim Reshetov] [ZEPPELIN-3194] docs
ce97551b9 [Maksim Reshetov] Merge remote-tracking branch 'upstream/master' into ZEPPELIN-3194
0784ba030 [Maksim Reshetov] [ZEPPELIN-3194] docs
e00ace2e3 [Maksim Reshetov] [ZEPPELIN-3194] completer tests
1aeb7ad0f [Maksim Reshetov] [ZEPPELIN-3194] pom license
c1c09af5c [Maksim Reshetov] [ZEPPELIN-3194] docs fix + unit tests for util
f6b71f9c2 [Maksim Reshetov] [ZEPPELIN-3194] is null fix


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/63c53fcc
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/63c53fcc
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/63c53fcc

Branch: refs/heads/master
Commit: 63c53fcc5979b8c2cbd8bae13de2561078780637
Parents: bfc93dc
Author: Maksim Reshetov <jm...@gmail.com>
Authored: Thu Mar 1 23:34:20 2018 +0500
Committer: Lee moon soo <mo...@apache.org>
Committed: Sun Mar 4 10:00:22 2018 -0800

----------------------------------------------------------------------
 docs/index.md                                   |   1 +
 docs/interpreter/sap.md                         | 123 ++++
 pom.xml                                         |   1 +
 sap/pom.xml                                     |  86 +++
 .../zeppelin/sap/UniverseInterpreter.java       | 215 ++++++
 .../zeppelin/sap/universe/UniverseClient.java   | 733 +++++++++++++++++++
 .../sap/universe/UniverseCompleter.java         | 344 +++++++++
 .../sap/universe/UniverseException.java         |  38 +
 .../zeppelin/sap/universe/UniverseInfo.java     |  60 ++
 .../zeppelin/sap/universe/UniverseNodeInfo.java |  85 +++
 .../sap/universe/UniverseNodeInfoCompleter.java | 183 +++++
 .../zeppelin/sap/universe/UniverseQuery.java    |  53 ++
 .../sap/universe/UniverseQueryPrompt.java       | 110 +++
 .../zeppelin/sap/universe/UniverseUtil.java     | 643 ++++++++++++++++
 sap/src/main/resources/interpreter-setting.json |  42 ++
 sap/src/main/resources/universe.keywords        |   1 +
 .../sap/universe/UniverseCompleterTest.java     | 134 ++++
 .../zeppelin/sap/universe/UniverseUtilTest.java | 371 ++++++++++
 .../zeppelin/completer/CompletionType.java      |   3 +-
 .../zeppelin/conf/ZeppelinConfiguration.java    |   3 +-
 .../notebook/paragraph/paragraph.controller.js  |   2 +-
 21 files changed, 3228 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/docs/index.md
----------------------------------------------------------------------
diff --git a/docs/index.md b/docs/index.md
index 3d42735..f00571e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -150,6 +150,7 @@ limitations under the License.
   * [Postgresql, HAWQ](./interpreter/postgresql.html)
   * [Python](./interpreter/python.html)
   * [R](./interpreter/r.html)
+  * [SAP](./interpreter/sap.html)
   * [Scalding](./interpreter/scalding.html)
   * [Scio](./interpreter/scio.html)
   * [Shell](./interpreter/Shell.html)

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/docs/interpreter/sap.md
----------------------------------------------------------------------
diff --git a/docs/interpreter/sap.md b/docs/interpreter/sap.md
new file mode 100644
index 0000000..be05aee
--- /dev/null
+++ b/docs/interpreter/sap.md
@@ -0,0 +1,123 @@
+---
+
+layout: page
+
+title: "SAP BusinessObjects Interpreter for Apache Zeppelin"
+
+description: "SAP BusinessObjects BI platform can simplify the lives of business users and IT staff. SAP BusinessObjects is based on universes. The universe contains dual-semantic layer model. The users make queries upon universes. This interpreter is new interface for universes."
+
+group: interpreter
+
+---
+
+<!--
+
+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.
+
+-->
+
+{% include JB/setup %}
+
+# SAP BusinessObjects (Universe) Interpreter for Apache Zeppelin
+
+<div id="toc"></div>
+
+## Overview
+
+[SAP BusinessObjects BI platform (universes)](https://help.sap.com/viewer/p/SAP_BUSINESSOBJECTS_BUSINESS_INTELLIGENCE_PLATFORM) can simplify the lives of business users and IT staff. SAP BusinessObjects is based on universes. The universe contains dual-semantic layer model. The users make queries upon universes. This interpreter is new interface for universes.
+
+*Disclaimer* SAP interpreter is not official interpreter for SAP BusinessObjects BI platform. It uses [BI Semantic Layer REST API](https://help.sap.com/viewer/5431204882b44fc98d56bd752e69f132/4.2.5/en-US/ec54808e6fdb101497906a7cb0e91070.html)
+
+This interpreter is not directly supported by SAP AG.
+
+Tested with versions 4.2SP3 (14.2.3.2220) and 4.2SP5. There is no support for filters in UNX-universes converted from old UNV format.
+
+The universe name must be unique.
+
+## Configuring SAP Universe Interpreter
+
+At the "Interpreters" menu, you can edit SAP interpreter or create new one. Zeppelin provides these properties for SAP.
+
+<table class="table-configuration">
+  <tr>
+    <th>Property Name</th>
+    <th>Value</th>
+    <th>Description</th>
+  </tr>
+  <tr>
+    <td>universe.api.url</td>
+    <td>http://localhost:6405/biprws</td>
+    <td>The base url for the SAP BusinessObjects BI platform. You have to edit "localhost" that you may use (ex. http://0.0.0.0:6405/biprws)</td>
+  </tr>
+  <tr>
+    <td>universe.authType</td>
+    <td>secEnterprise</td>
+    <td>The type of authentication for API of Universe. Available values: secEnterprise, secLDAP, secWinAD, secSAPR3</td>
+  </tr>
+  <tr>
+    <td>universe.password</td>
+    <td></td>
+    <td>The BI platform user password</td>
+  </tr>
+  <tr>
+    <td>universe.user</td>
+    <td>Administrator</td>
+    <td>The BI platform user login</td>
+  </tr>
+</table>
+
+![SAP Interpreter Setting]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/sap-interpreter-setting.png)
+
+### How to use
+
+<li> Choose the universe
+<li> Choose dimensions and measures in `select` statement
+<li> Define conditions in `where` statement
+You can compare two dimensions/measures or use Filter (without value). 
+Dimesions/Measures can be compared with static values, may be `is null` or `is not null`, contains or not in list.
+Available the nested conditions (using braces "()"). "and" operator have more priority than "or". 
+
+
+If generated query contains promtps, then promtps will appear as dynamic form after paragraph submitting.
+
+Example query
+
+```
+%sap
+
+universe [Universe Name];
+
+select
+
+  [Folder1].[Dimension2],
+
+  [Folder2].[Dimension3],
+
+  [Measure1]
+
+where
+
+  [Filter1]
+
+  and [Date] > '2018-01-01 00:00:00'
+
+  and [Folder1].[Dimension4] is not null
+
+  and [Folder1].[Dimension5] in ('Value1', 'Value2');
+```
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 6ce20aa..dc6d64b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,7 @@
     <module>zeppelin-server</module>
     <module>zeppelin-jupyter</module>
     <module>zeppelin-distribution</module>
+    <module>sap</module>
   </modules>
 
   <properties>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/pom.xml
----------------------------------------------------------------------
diff --git a/sap/pom.xml b/sap/pom.xml
new file mode 100644
index 0000000..f0bf317
--- /dev/null
+++ b/sap/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>interpreter-parent</artifactId>
+        <groupId>org.apache.zeppelin</groupId>
+        <version>0.9.0-SNAPSHOT</version>
+        <relativePath>../interpreter-parent</relativePath>
+    </parent>
+
+    <groupId>org.apache.zeppelin</groupId>
+    <artifactId>sap</artifactId>
+    <packaging>jar</packaging>
+    <version>0.9.0-SNAPSHOT</version>
+    <name>Zeppelin: Sap</name>
+    <description>Zeppelin SAP support</description>
+
+    <properties>
+        <interpreter.name>sap</interpreter.name>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>zeppelin-interpreter</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-enforcer-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <artifactId>maven-dependency-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java b/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java
new file mode 100644
index 0000000..1fdb6d6
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java
@@ -0,0 +1,215 @@
+/*
+ * 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.zeppelin.sap;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.zeppelin.interpreter.Interpreter;
+import org.apache.zeppelin.interpreter.InterpreterContext;
+import org.apache.zeppelin.interpreter.InterpreterException;
+import org.apache.zeppelin.interpreter.InterpreterResult;
+import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
+import org.apache.zeppelin.sap.universe.*;
+
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * SAP Universe interpreter for Zeppelin.
+ */
+public class UniverseInterpreter extends Interpreter {
+
+  public UniverseInterpreter(Properties properties) {
+    super(properties);
+  }
+
+  private UniverseClient client;
+  private UniverseUtil universeUtil;
+  private UniverseCompleter universeCompleter;
+
+  private static final String EMPTY_COLUMN_VALUE = StringUtils.EMPTY;
+  private static final char WHITESPACE = ' ';
+  private static final char NEWLINE = '\n';
+  private static final char TAB = '\t';
+  private static final String TABLE_MAGIC_TAG = "%table ";
+
+  @Override
+  public void open() throws InterpreterException {
+    String user = getProperty("universe.user");
+    String password = getProperty("universe.password");
+    String apiUrl = getProperty("universe.api.url");
+    String authType = getProperty("universe.authType");
+    this.client = new UniverseClient(user, password, apiUrl, authType);
+    this.universeUtil = new UniverseUtil();
+  }
+
+  @Override
+  public void close() throws InterpreterException {
+    try {
+      client.close();
+    } catch (Exception e) {
+      throw new InterpreterException(e.getCause());
+    }
+  }
+
+  @Override
+  public InterpreterResult interpret(String st, InterpreterContext context)
+      throws InterpreterException {
+    try {
+      InterpreterResult interpreterResult = new InterpreterResult(InterpreterResult.Code.SUCCESS);
+      String paragraphId = context.getParagraphId();
+      String token = client.getToken(paragraphId);
+      client.loadUniverses(token);
+      UniverseQuery universeQuery = universeUtil.convertQuery(st, client, token);
+      String queryId = client.createQuery(token, universeQuery);
+      // process parameters
+      List<UniverseQueryPrompt> parameters = client.getParameters(token, queryId);
+
+      for (UniverseQueryPrompt parameter : parameters) {
+        Object value = context.getGui().getParams().get(parameter.getName());
+        if (value != null) {
+          parameter.setValue(value.toString());
+        }
+        context.getGui().textbox(parameter.getName(), StringUtils.EMPTY);
+      }
+
+      if (!parameters.isEmpty() && parameters.size() != context.getGui().getParams().size()) {
+        client.deleteQuery(token, queryId);
+        interpreterResult.add("Set parameters");
+        return interpreterResult;
+      }
+
+      if (!parameters.isEmpty()) {
+        client.setParametersValues(token, queryId, parameters);
+      }
+
+      // get results
+      List<List<String>> results = client.getResults(token, queryId);
+      String table = formatResults(results);
+      // remove query
+      client.deleteQuery(token, queryId);
+      interpreterResult.add(table);
+      return interpreterResult;
+    } catch (Exception e) {
+      throw new InterpreterException(e.getMessage(), e);
+    } finally {
+      try {
+        client.closeSession(context.getParagraphId());
+      } catch (Exception e) {
+        logger.error("Error close SAP session", e );
+      }
+    }
+  }
+
+  @Override
+  public void cancel(InterpreterContext context) throws InterpreterException {
+    try {
+      client.closeSession(context.getParagraphId());
+    } catch (Exception e) {
+      logger.error("Error close SAP session", e );
+    }
+  }
+
+  @Override
+  public FormType getFormType() throws InterpreterException {
+    return FormType.NATIVE;
+  }
+
+  @Override
+  public int getProgress(InterpreterContext context) throws InterpreterException {
+    return 0;
+  }
+
+  @Override
+  public List<InterpreterCompletion> completion(String buf, int cursor,
+                                                InterpreterContext interpreterContext)
+      throws InterpreterException {
+    List<InterpreterCompletion> candidates = new ArrayList<>();
+
+    try {
+      universeCompleter = createOrUpdateUniverseCompleter(interpreterContext, buf, cursor);
+      universeCompleter.complete(buf, cursor, candidates);
+    } catch (UniverseException e) {
+      logger.error("Error update completer", e );
+    }
+
+    return candidates;
+  }
+
+  private String formatResults(List<List<String>> results) {
+    StringBuilder msg = new StringBuilder();
+    if (results != null) {
+      msg.append(TABLE_MAGIC_TAG);
+      for (int i = 0; i < results.size(); i++) {
+        List<String> items = results.get(i);
+        for (int j = 0; j < items.size(); j++) {
+          if (j > 0) {
+            msg.append(TAB);
+          }
+          msg.append(replaceReservedChars(items.get(j)));
+        }
+        msg.append(NEWLINE);
+      }
+    }
+
+    return msg.toString();
+  }
+
+  private String replaceReservedChars(String str) {
+    if (str == null) {
+      return EMPTY_COLUMN_VALUE;
+    }
+    return str.replace(TAB, WHITESPACE).replace(NEWLINE, WHITESPACE);
+  }
+
+  private UniverseCompleter createOrUpdateUniverseCompleter(InterpreterContext interpreterContext,
+                                                            final String buf, final int cursor)
+      throws UniverseException {
+    final UniverseCompleter completer;
+    if (universeCompleter == null) {
+      completer = new UniverseCompleter(3600);
+    } else {
+      completer = universeCompleter;
+    }
+    try {
+      final String token = client.getToken(interpreterContext.getParagraphId());
+      ExecutorService executorService = Executors.newFixedThreadPool(1);
+      executorService.execute(new Runnable() {
+        @Override
+        public void run() {
+          completer.createOrUpdate(client, token, buf, cursor);
+        }
+      });
+
+      executorService.shutdown();
+
+      executorService.awaitTermination(10, TimeUnit.SECONDS);
+    } catch (InterruptedException e) {
+      logger.warn("Completion timeout", e);
+    } finally {
+      try {
+        client.closeSession(interpreterContext.getParagraphId());
+      } catch (Exception e) {
+        logger.error("Error close SAP session", e );
+      }
+    }
+    return completer;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java
new file mode 100644
index 0000000..b60eac4
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java
@@ -0,0 +1,733 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import com.sun.org.apache.xpath.internal.NodeSet;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Client for API  SAP Universe
+ */
+public class UniverseClient {
+
+  private static Logger logger = LoggerFactory.getLogger(UniverseClient.class);
+  private static final String TOKEN_HEADER = "X-SAP-LogonToken";
+  private static final String EL_FOLDER = "folder";
+  private static final String EL_ITEM = "item";
+  private static final String EL_NAME = "name";
+  private static final String EL_PATH = "path";
+  private static final String EL_ID = "id";
+  private static final String EL_TECH_NAME = "technicalName";
+  private static final String EL_ANSWER = "answer";
+  private static final String EL_INFO = "info";
+  private Map<String, String> tokens = new HashMap();
+  private static final long DAY = 1000 * 60 * 60 * 24;
+  private CloseableHttpClient httpClient;
+  private String user;
+  private String password;
+  private String apiUrl;
+  private String authType;
+  private Header[] commonHeaders = {
+    new BasicHeader("Accept", "application/xml"),
+    new BasicHeader("Content-Type", "application/xml")
+  };
+  // <name, id>
+  private final Map<String, UniverseInfo> universesMap = new ConcurrentHashMap();
+  private final Map<String, Map<String, UniverseNodeInfo>> universeInfosMap =
+      new ConcurrentHashMap();
+  // for update the data (which was not updated a long time)
+  private long universesUpdated = 0;
+  private Map<String, Long> universesInfoUpdatedMap = new HashMap<>();
+
+  private final String loginRequestTemplate = "<attrs xmlns=\"http://www.sap.com/rws/bip\">\n"
+      + "<attr name=\"userName\" type=\"string\">%s</attr>\n"
+      + "<attr name=\"password\" type=\"string\">%s</attr>\n"
+      + "<attr name=\"auth\" type=\"string\" "
+      + "possibilities=\"secEnterprise,secLDAP,secWinAD,secSAPR3\">%s</attr>\n" + "</attrs>";
+  private final String createQueryRequestTemplate =
+      "<query xmlns=\"http://www.sap.com/rws/sl/universe\" dataSourceType=\"%s\" " +
+          "dataSourceId=\"%s\">\n" +
+          "<querySpecification version=\"1.0\">\n" +
+          "  <queryData>\n%s\n" +
+          "     %s\n" +
+          "</queryData>\n" +
+          "</querySpecification>\n" +
+      "</query>\n";
+  private final String filterPartTemplate = "<filterPart>%s\n</filterPart>";
+  private final String errorMessageTemplate = "%s\n\n%s";
+  private final String parameterTemplate = "<parameter type=\"prompt\">\n" +
+      "%s\n" +
+      "%s\n" +
+      "%s\n" +
+      "%s\n" +
+      "</parameter>\n";
+  private final String parameterAnswerTemplate = "<answer constrained=\"%s\" type=\"%s\">\n" +
+      "            <info cardinality=\"%s\" keepLastValues=\"%s\"></info>\n" +
+      "               <values>\n" + "     " +
+      "                 <value>%s</value>\n" +
+      "              </values>\n" +
+      "        </answer>\n";
+
+  public UniverseClient(String user, String password, String apiUrl, String authType) {
+    RequestConfig requestConfig = RequestConfig.custom()
+        .setConnectTimeout(20 * 60 * 1000)
+        .setSocketTimeout(20 * 60 * 1000)
+        .build();
+    PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+    cm.setMaxTotal(100);
+    cm.setDefaultMaxPerRoute(100);
+    cm.closeIdleConnections(10, TimeUnit.MINUTES);
+    httpClient = HttpClientBuilder.create()
+        .setConnectionManager(cm)
+        .setDefaultRequestConfig(requestConfig)
+        .build();
+
+    this.user = user;
+    this.password = password;
+    this.authType = authType;
+    if (StringUtils.isNotBlank(apiUrl)) {
+      this.apiUrl = apiUrl.replaceAll("/$", "");
+    }
+  }
+
+  public void close() throws UniverseException {
+    for (String s : tokens.keySet()) {
+      closeSession(s);
+    }
+    try {
+      httpClient.close();
+    } catch (Exception e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " +
+          "(close all): Error close HTTP client", ExceptionUtils.getStackTrace(e)));
+    }
+
+  }
+
+  public String createQuery(String token, UniverseQuery query) throws UniverseException {
+    try {
+      HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/sl/v1/queries"));
+      setHeaders(httpPost, token);
+      String where = StringUtils.isNotBlank(query.getWhere()) ?
+          String.format(filterPartTemplate, query.getWhere()) : StringUtils.EMPTY;
+      httpPost.setEntity(new StringEntity(
+          String.format(createQueryRequestTemplate, query.getUniverseInfo().getType(),
+              query.getUniverseInfo().getId(), query.getSelect(), where), "UTF-8"));
+      HttpResponse response = httpClient.execute(httpPost);
+
+      if (response.getStatusLine().getStatusCode() == 200) {
+        return getValue(EntityUtils.toString(response.getEntity()), "//success/id");
+      }
+
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(create query): Request failed\n", EntityUtils.toString(response.getEntity())));
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(create query): Request failed", ExceptionUtils.getStackTrace(e)));
+    } catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(create query): Response processing failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  public void deleteQuery(String token, String queryId) throws UniverseException {
+    try {
+      if (StringUtils.isNotBlank(queryId)) {
+        HttpDelete httpDelete = new HttpDelete(String.format("%s%s%s", apiUrl, "/sl/v1/queries/",
+            queryId));
+        setHeaders(httpDelete, token);
+        httpClient.execute(httpDelete);
+      }
+    } catch (Exception e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " +
+          "(delete query): Request failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  public List<List<String>> getResults(String token, String queryId) throws UniverseException {
+    HttpGet httpGet = new HttpGet(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/",
+        queryId, "/data.svc/Flows0"));
+    setHeaders(httpGet, token);
+    HttpResponse response = null;
+    try {
+      response = httpClient.execute(httpGet);
+      if (response.getStatusLine().getStatusCode() != 200) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get results): Request failed\n", EntityUtils.toString(response.getEntity())));
+      }
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " +
+          "(get results): Request failed", ExceptionUtils.getStackTrace(e)));
+    }
+
+    try (InputStream xmlStream = response.getEntity().getContent()) {
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      DocumentBuilder builder = factory.newDocumentBuilder();
+      Document doc = builder.parse(xmlStream);
+      XPathFactory xPathfactory = XPathFactory.newInstance();
+      XPath xpath = xPathfactory.newXPath();
+      XPathExpression expr = xpath.compile("//feed/entry/content/properties");
+      NodeList resultsNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
+      if (resultsNodes != null) {
+        return parseResults(resultsNodes);
+      } else {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get results): Response processing failed"));
+      }
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get results): Request failed", ExceptionUtils.getStackTrace(e)));
+    } catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get results): Response processing failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  public String getToken(String paragraphId) throws UniverseException {
+    try {
+      if (tokens.containsKey(paragraphId)) {
+        return tokens.get(paragraphId);
+      }
+      HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/logon/long"));
+      setHeaders(httpPost);
+
+      httpPost.setEntity(new StringEntity(
+          String.format(loginRequestTemplate, user, password, authType), "UTF-8"));
+      HttpResponse response = httpClient.execute(httpPost);
+      String result = null;
+      if (response.getStatusLine().getStatusCode() == 200) {
+        result = getValue(EntityUtils.toString(response.getEntity()),
+            "//content/attrs/attr[@name=\"logonToken\"]");
+        tokens.put(paragraphId, result);
+      }
+
+      return result;
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get token): Request failed", ExceptionUtils.getStackTrace(e)));
+    } catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get token): Response processing failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  public boolean closeSession(String paragraphId) throws UniverseException {
+    try {
+      if (tokens.containsKey(paragraphId)) {
+        HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/logoff"));
+        setHeaders(httpPost, tokens.get(paragraphId));
+        HttpResponse response = httpClient.execute(httpPost);
+        if (response.getStatusLine().getStatusCode() == 200) {
+          return true;
+        }
+      }
+
+      return false;
+    } catch (Exception e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(close session): Request failed", ExceptionUtils.getStackTrace(e)));
+    } finally {
+      tokens.remove(paragraphId);
+    }
+  }
+
+  public UniverseInfo getUniverseInfo(String universeName) {
+    return universesMap.get(universeName);
+  }
+
+  public Map<String, UniverseNodeInfo> getUniverseNodesInfo(String token, String universeName)
+      throws UniverseException {
+    UniverseInfo universeInfo = universesMap.get(universeName);
+    if (universeInfo != null && StringUtils.isNotBlank(universeInfo.getId())) {
+      Map<String, UniverseNodeInfo> universeNodeInfoMap = universeInfosMap.get(universeName);
+      if (universeNodeInfoMap != null && universesInfoUpdatedMap.containsKey(universeName) &&
+          !isExpired(universesInfoUpdatedMap.get(universeName))) {
+        return universeNodeInfoMap;
+      } else {
+        universeNodeInfoMap = new HashMap<>();
+      }
+      try {
+        HttpGet httpGet =
+            new HttpGet(String.format("%s%s%s", apiUrl, "/sl/v1/universes/", universeInfo.getId()));
+        setHeaders(httpGet, token);
+        HttpResponse response = httpClient.execute(httpGet);
+
+        if (response.getStatusLine().getStatusCode() == 200) {
+          try (InputStream xmlStream = response.getEntity().getContent()) {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.parse(xmlStream);
+            XPathFactory xPathfactory = XPathFactory.newInstance();
+            XPath xpath = xPathfactory.newXPath();
+            XPathExpression expr = xpath.compile("//outline/folder");
+            XPathExpression exprRootItems = xpath.compile("//outline/item");
+            NodeList universeInfoNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
+            NodeList universeRootInfoNodes =
+                (NodeList) exprRootItems.evaluate(doc, XPathConstants.NODESET);
+            if (universeInfoNodes != null) {
+              parseUniverseInfo(universeInfoNodes, universeNodeInfoMap);
+            }
+            if (universeRootInfoNodes != null) {
+              parseUniverseInfo(universeRootInfoNodes, universeNodeInfoMap);
+            }
+          } catch (Exception e) {
+            throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+                + "(get universe nodes info): Response processing failed",
+                ExceptionUtils.getStackTrace(e)));
+          }
+        }
+      } catch (IOException e) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get universe nodes info): Request failed", ExceptionUtils.getStackTrace(e)));
+      }
+      universeInfosMap.put(universeName, universeNodeInfoMap);
+      universesInfoUpdatedMap.put(universeName, System.currentTimeMillis());
+
+      return universeNodeInfoMap;
+    }
+    return Collections.emptyMap();
+
+  }
+
+  public void loadUniverses(String token) throws UniverseException {
+    if (universesMap.isEmpty() || universesUpdated == 0 || isExpired(universesUpdated)) {
+      Map<String, UniverseInfo> universes = new ConcurrentHashMap();
+      loadUniverses(token, 0, universes);
+      universesMap.clear();
+      universesMap.putAll(universes);
+      universesUpdated = System.currentTimeMillis();
+    }
+  }
+
+  public void cleanUniverses() {
+    universesMap.clear();
+  }
+
+  public void removeUniverseInfo(String universe) {
+    universeInfosMap.remove(universe);
+  }
+
+  public Map<String, UniverseInfo> getUniversesMap() {
+    return universesMap;
+  }
+
+  public List<UniverseQueryPrompt> getParameters(String token, String queryId)
+      throws UniverseException {
+    HttpGet httpGet = new HttpGet(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/",
+        queryId, "/parameters"));
+    setHeaders(httpGet, token);
+    HttpResponse response = null;
+    try {
+      response = httpClient.execute(httpGet);
+      if (response.getStatusLine().getStatusCode() != 200) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get parameters): Request failed\n", EntityUtils.toString(response.getEntity())));
+      }
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " +
+          "(get parameters): Request failed", ExceptionUtils.getStackTrace(e)));
+    }
+
+    try (InputStream xmlStream = response.getEntity().getContent()) {
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      DocumentBuilder builder = factory.newDocumentBuilder();
+      Document doc = builder.parse(xmlStream);
+      XPathFactory xPathfactory = XPathFactory.newInstance();
+      XPath xpath = xPathfactory.newXPath();
+      XPathExpression expr = xpath.compile("//parameters/parameter");
+      NodeList parametersNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
+      if (parametersNodes != null) {
+        return parseParameters(parametersNodes);
+      } else {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get parameters): Response processing failed"));
+      }
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get parameters): Response processing failed", ExceptionUtils.getStackTrace(e)));
+    } catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get parameters): Response processing failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  public void setParametersValues(String token, String queryId,
+                                  List<UniverseQueryPrompt> parameters) throws UniverseException {
+    HttpPut httpPut = new HttpPut(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/",
+        queryId, "/parameters"));
+    setHeaders(httpPut, token);
+    HttpResponse response = null;
+    try {
+      StringBuilder request = new StringBuilder();
+      request.append("<parameters>\n");
+      for (UniverseQueryPrompt parameter : parameters) {
+        String answer = String.format(parameterAnswerTemplate, parameter.getConstrained(),
+            parameter.getType(), parameter.getCardinality(), parameter.getKeepLastValues(),
+            parameter.getValue());
+        String id = parameter.getId() != null ? String.format("<id>%s</id>\n", parameter.getId()) :
+            StringUtils.EMPTY;
+        String technicalName = parameter.getTechnicalName() != null ?
+            String.format("<technicalName>%s</technicalName>\n", parameter.getTechnicalName()) :
+            StringUtils.EMPTY;
+        String name = parameter.getTechnicalName() != null ?
+            String.format("<name>%s</name>\n", parameter.getName()) :
+            StringUtils.EMPTY;
+        request.append(String.format(parameterTemplate, id, technicalName, name, answer));
+      }
+      request.append("</parameters>\n");
+
+      httpPut.setEntity(new StringEntity(request.toString(), "UTF-8"));
+
+      response = httpClient.execute(httpPut);
+      if (response.getStatusLine().getStatusCode() != 200) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(set parameters): Request failed\n", EntityUtils.toString(response.getEntity())));
+      }
+    } catch (IOException e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " +
+          "(set parameters): Request failed", ExceptionUtils.getStackTrace(e)));
+    }
+  }
+
+  private void loadUniverses(String token, int offset, Map<String, UniverseInfo> universesMap)
+      throws UniverseException {
+    int limit = 50;
+    HttpGet httpGet = new HttpGet(String.format("%s%s?offset=%s&limit=%s", apiUrl,
+        "/sl/v1/universes",
+        offset, limit));
+    setHeaders(httpGet, token);
+    HttpResponse response = null;
+    try {
+      response = httpClient.execute(httpGet);
+    } catch (Exception e) {
+      throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+          + "(get universes): Request failed", ExceptionUtils.getStackTrace(e)));
+    }
+    if (response != null && response.getStatusLine().getStatusCode() == 200) {
+      try (InputStream xmlStream = response.getEntity().getContent()) {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        Document doc = builder.parse(xmlStream);
+        XPathFactory xPathfactory = XPathFactory.newInstance();
+        XPath xpath = xPathfactory.newXPath();
+        XPathExpression expr = xpath.compile("//universe");
+        NodeList universesNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
+        if (universesNodes != null) {
+          int count = universesNodes.getLength();
+          for (int i = 0; i < count; i++) {
+            Node universe = universesNodes.item(i);
+            if (universe.hasChildNodes()) {
+              NodeList universeParameters = universe.getChildNodes();
+              int parapetersCount = universeParameters.getLength();
+              String id = null;
+              String name = null;
+              String type = null;
+              for (int j = 0; j < parapetersCount; j++) {
+                Node parameterNode = universeParameters.item(j);
+                parameterNode.getNodeName();
+                if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
+                  if (parameterNode.getNodeName().equalsIgnoreCase("id")) {
+                    id = parameterNode.getTextContent();
+                    continue;
+                  }
+                  if (parameterNode.getNodeName().equalsIgnoreCase("name")) {
+                    name = parameterNode.getTextContent();
+                    continue;
+                  }
+                  if (parameterNode.getNodeName().equalsIgnoreCase("type")) {
+                    type = parameterNode.getTextContent();
+                    continue;
+                  }
+                }
+              }
+              if (StringUtils.isNotBlank(type)) {
+                name = name.replaceAll(String.format("\\.%s$", type), StringUtils.EMPTY);
+              }
+              universesMap.put(name, new UniverseInfo(id, name, type));
+            }
+          }
+          if (count == limit) {
+            offset += limit;
+            loadUniverses(token, offset, universesMap);
+          }
+        }
+      } catch (IOException e) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+                + "(get universes): Response processing failed", ExceptionUtils.getStackTrace(e)));
+      } catch (ParserConfigurationException | SAXException | XPathExpressionException e) {
+        throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient "
+            + "(get universes): Response processing failed", ExceptionUtils.getStackTrace(e)));
+      }
+    }
+  }
+
+  private boolean isExpired(Long lastUpdated) {
+    if (lastUpdated == null || System.currentTimeMillis() - lastUpdated > DAY) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private void setHeaders(HttpRequestBase request) {
+    setHeaders(request, null);
+  }
+
+  private void setHeaders(HttpRequestBase request, String token) {
+    request.setHeaders(commonHeaders);
+    if (StringUtils.isNotBlank(token)) {
+      request.addHeader(TOKEN_HEADER, token);
+    }
+  }
+
+  private String getValue(String response, String xPathString) throws ParserConfigurationException,
+      IOException, SAXException, XPathExpressionException {
+    try (InputStream xmlStream = new ByteArrayInputStream(response.getBytes())) {
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      DocumentBuilder builder = factory.newDocumentBuilder();
+      Document doc = builder.parse(xmlStream);
+      XPathFactory xPathfactory = XPathFactory.newInstance();
+      XPath xpath = xPathfactory.newXPath();
+      XPathExpression expr = xpath.compile(xPathString);
+      Node tokenNode = (Node) expr.evaluate(doc, XPathConstants.NODE);
+      if (tokenNode != null) {
+        return tokenNode.getTextContent();
+      }
+    }
+    return null;
+  }
+
+  private List<UniverseQueryPrompt> parseParameters(NodeList parametersNodeList) {
+    List<UniverseQueryPrompt> parameters = new ArrayList<>();
+    if (parametersNodeList != null) {
+      int count = parametersNodeList.getLength();
+      for (int i = 0; i < count; i++) {
+        Node parameterNode = parametersNodeList.item(i);
+        Node type = parameterNode.getAttributes().getNamedItem("type");
+        if (type != null && type.getTextContent().equalsIgnoreCase("prompt") &&
+            parameterNode.hasChildNodes()) {
+          NodeList parameterInfoNodes = parameterNode.getChildNodes();
+          int childNodesCount = parameterInfoNodes.getLength();
+          String name = null;
+          Integer id = null;
+          String cardinality = null;
+          String constrained = null;
+          String valueType = null;
+          String technicalName = null;
+          String keepLastValues = null;
+          for (int j = 0; j < childNodesCount; j++) {
+            Node childNode = parameterInfoNodes.item(j);
+            String childNodeName = childNode.getNodeName();
+            if (childNodeName.equalsIgnoreCase(EL_NAME)) {
+              name = childNode.getTextContent();
+              continue;
+            }
+            if (childNodeName.equalsIgnoreCase(EL_ID)) {
+              id = Integer.parseInt(childNode.getTextContent());
+              continue;
+            }
+            if (childNodeName.equalsIgnoreCase(EL_TECH_NAME)) {
+              technicalName = childNode.getTextContent();
+              continue;
+            }
+            if (childNodeName.equalsIgnoreCase(EL_ANSWER)) {
+              NamedNodeMap answerAttributes = childNode.getAttributes();
+              if (answerAttributes.getNamedItem("constrained") != null) {
+                constrained = answerAttributes.getNamedItem("constrained").getTextContent();
+              }
+              if (answerAttributes.getNamedItem("type") != null) {
+                valueType = answerAttributes.getNamedItem("type").getTextContent();
+              }
+              NodeList answerNodes = childNode.getChildNodes();
+              int answerCount = answerNodes.getLength();
+              for (int k = 0; k < answerCount; k++) {
+                Node answerChildNode = answerNodes.item(k);
+                String answerChildNodeName = answerChildNode.getNodeName();
+                if (answerChildNodeName.equalsIgnoreCase(EL_INFO)) {
+                  NamedNodeMap infoAttributes = answerChildNode.getAttributes();
+                  if (infoAttributes.getNamedItem("cardinality") != null) {
+                    cardinality = infoAttributes.getNamedItem("cardinality").getTextContent();
+                  }
+                  if (infoAttributes.getNamedItem("keepLastValues") != null) {
+                    keepLastValues = infoAttributes.getNamedItem("keepLastValues").getTextContent();
+                  }
+                  break;
+                }
+              }
+              continue;
+            }
+          }
+          if (name != null && id != null && cardinality != null) {
+            parameters.add(new UniverseQueryPrompt(id, name, cardinality, constrained, valueType,
+                technicalName, keepLastValues));
+            break;
+          }
+        }
+      }
+    }
+
+    return parameters;
+  }
+
+  private List<List<String>> parseResults(NodeList resultsNodeList) {
+    List<List<String>> results = new ArrayList<>();
+    if (resultsNodeList != null) {
+      int count = resultsNodeList.getLength();
+      for (int i = 0; i < count; i++) {
+        Node node = resultsNodeList.item(i);
+        if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) {
+          NodeList properties = node.getChildNodes();
+          if (properties != null) {
+            int countProperties = properties.getLength();
+            List<String> headers = new ArrayList<>();
+            List<String> row = new ArrayList<>();
+            // first property is id
+            for (int j = 1; j < countProperties; j++) {
+              Node propertyNode = properties.item(j);
+              if (i == 0) {
+                headers.add(propertyNode.getNodeName().replaceAll("^\\w*:", StringUtils.EMPTY));
+              }
+              row.add(propertyNode.getTextContent());
+            }
+            if (i == 0) {
+              results.add(headers);
+            }
+            results.add(row);
+          }
+        }
+      }
+    }
+
+    return results;
+  }
+
+  private void parseUniverseInfo(NodeList universeInfoNodes, Map<String, UniverseNodeInfo> nodes) {
+    if (universeInfoNodes != null) {
+      int count = universeInfoNodes.getLength();
+      for (int i = 0; i < count; i++) {
+        Node node = universeInfoNodes.item(i);
+        if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) {
+          String name = node.getNodeName();
+          NodeList childNodes = node.getChildNodes();
+          int childNodesCount = childNodes.getLength();
+          if (name.equalsIgnoreCase(EL_FOLDER)) {
+            NodeSet list = new NodeSet();
+            for (int j = 0; j < childNodesCount; j++) {
+              Node childNode = childNodes.item(j);
+              if (childNode.getNodeType() == Node.ELEMENT_NODE && childNode.hasChildNodes()) {
+                String childNodeName = childNode.getNodeName();
+                if (childNodeName.equalsIgnoreCase(EL_FOLDER)
+                    || childNodeName.equalsIgnoreCase(EL_ITEM)) {
+                  list.addNode(childNode);
+                }
+              }
+            }
+            if (list.getLength() > 0) {
+              parseUniverseInfo(list, nodes);
+            }
+          } else if (name.equalsIgnoreCase(EL_ITEM)) {
+            String nodeId = null;
+            String nodeName = null;
+            String nodePath = null;
+            for (int j = 0; j < childNodesCount; j++) {
+              Node childNode = childNodes.item(j);
+              if (childNode.getNodeType() == Node.ELEMENT_NODE) {
+                String childNodeName = childNode.getNodeName();
+                if (childNodeName.equalsIgnoreCase(EL_NAME)) {
+                  nodeName = childNode.getTextContent();
+                  continue;
+                }
+                if (childNodeName.equalsIgnoreCase(EL_ID)) {
+                  nodeId = childNode.getTextContent();
+                  continue;
+                }
+                if (childNodeName.equalsIgnoreCase(EL_PATH)) {
+                  nodePath = childNode.getTextContent();
+                  continue;
+                }
+              }
+            }
+            String folder = null;
+            StringBuilder key = new StringBuilder();
+            if (StringUtils.isNotBlank(nodeName)) {
+              String nodeType = null;
+              if (StringUtils.isNotBlank(nodePath)) {
+                String[] parts = nodePath.split("\\\\");
+                List<String> path = new ArrayList();
+                for (String part : parts) {
+                  String[] p = part.split("\\|");
+                  if (p.length == 2) {
+                    if (p[1].equalsIgnoreCase("folder")) {
+                      path.add(p[0]);
+                    } else {
+                      nodeName = p[0];
+                      nodeType = p[1];
+                    }
+                  }
+                }
+                folder = StringUtils.join(path, "\\");
+                if (path.isEmpty()) {
+                  key.append(String.format("[%s]", nodeName));
+                } else {
+                  key.append("[");
+                  key.append(StringUtils.join(path, "].["));
+                  key.append(String.format("].[%s]", nodeName));
+                }
+              }
+              nodes.put(key.toString(),
+                  new UniverseNodeInfo(nodeId, nodeName, nodeType, folder, nodePath));
+            }
+          }
+        }
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java
new file mode 100644
index 0000000..e67011b
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java
@@ -0,0 +1,344 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import jline.console.completer.ArgumentCompleter.ArgumentList;
+import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter;
+import org.apache.commons.lang.StringUtils;
+import org.apache.zeppelin.completer.CachedCompleter;
+import org.apache.zeppelin.completer.CompletionType;
+import org.apache.zeppelin.completer.StringsCompleter;
+import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * SAP Universe auto complete functionality.
+ */
+public class UniverseCompleter {
+
+  private static Logger logger = LoggerFactory.getLogger(UniverseCompleter.class);
+
+  private static final String KEYWORD_SPLITERATOR = ",";
+  public static final String CLEAN_NAME_REGEX = "\\[|\\]";
+  public static final Character START_NAME = '[';
+  public static final Character END_NAME = ']';
+  public static final String KW_UNIVERSE = "universe";
+  public static final String TYPE_FOLDER = "folder";
+
+  private static final Comparator nodeInfoComparator = new Comparator<UniverseNodeInfo>() {
+    @Override
+    public int compare(UniverseNodeInfo o1, UniverseNodeInfo o2) {
+      if (o1.getType().equalsIgnoreCase(TYPE_FOLDER)
+          && o2.getType().equalsIgnoreCase(TYPE_FOLDER)) {
+        return o1.getName().compareToIgnoreCase(o2.getName());
+      }
+      if (o1.getType().equalsIgnoreCase(TYPE_FOLDER)) {
+        return -1;
+      }
+      if (o2.getType().equalsIgnoreCase(TYPE_FOLDER)) {
+        return 1;
+      }
+      if (!o1.getType().equalsIgnoreCase(o2.getType())) {
+        return o1.getType().compareToIgnoreCase(o2.getType());
+      } else {
+
+        return o1.getName().compareToIgnoreCase(o2.getName());
+      }
+    }
+  };
+
+  /**
+   * Delimiter that can split keyword list
+   */
+  private WhitespaceArgumentDelimiter sqlDelimiter = new WhitespaceArgumentDelimiter() {
+
+    private Pattern pattern = Pattern.compile(",|;");
+
+    @Override
+    public boolean isDelimiterChar(CharSequence buffer, int pos) {
+      char c = buffer.charAt(pos);
+      boolean endName = false;
+      for (int i = pos; i > 0; i--) {
+        char ch = buffer.charAt(i);
+        if (ch == '\n') {
+          break;
+        }
+        if (ch == START_NAME && !endName) {
+          return false;
+        }
+        if (ch == END_NAME) {
+          break;
+        }
+      }
+      return pattern.matcher(StringUtils.EMPTY + buffer.charAt(pos)).matches()
+              || super.isDelimiterChar(buffer, pos);
+    }
+  };
+
+  /**
+   * Universe completer
+   */
+  private CachedCompleter universeCompleter;
+
+  /**
+   * Keywords completer
+   */
+  private CachedCompleter keywordCompleter;
+
+  /**
+   * UniverseInfo completers
+   */
+  private Map<String, CachedCompleter> universeInfoCompletersMap = new HashMap<>();
+
+  private int ttlInSeconds;
+
+  public UniverseCompleter(int ttlInSeconds) {
+    this.ttlInSeconds = ttlInSeconds;
+  }
+
+  public int complete(String buffer, int cursor, List<InterpreterCompletion> candidates) {
+    CursorArgument cursorArgument = parseCursorArgument(buffer, cursor);
+
+    String argument = cursorArgument.getCursorArgumentPartForComplete();
+    if (cursorArgument.isUniverseNamePosition()) {
+      List<CharSequence> universeCandidates = new ArrayList<>();
+      universeCompleter.getCompleter().complete(argument, argument.length(),
+          universeCandidates);
+      addCompletions(candidates, universeCandidates, CompletionType.universe.name());
+      return universeCandidates.size();
+    }
+
+    if (cursorArgument.isUniverseNodePosition()) {
+      List universeNodeCandidates = new ArrayList();
+      CachedCompleter completer = universeInfoCompletersMap.get(cursorArgument.getUniverse());
+      if (completer != null) {
+        completer.getCompleter().complete(argument, argument.length(), universeNodeCandidates);
+      }
+      Collections.sort(universeNodeCandidates, nodeInfoComparator);
+      addCompletions(candidates, universeNodeCandidates);
+      return universeNodeCandidates.size();
+    }
+
+    List<CharSequence> keywordCandidates = new ArrayList<>();
+    keywordCompleter.getCompleter().complete(argument,
+        argument.length() > 0 ? argument.length() : 0, keywordCandidates);
+    addCompletions(candidates, keywordCandidates, CompletionType.keyword.name());
+
+    return keywordCandidates.size();
+  }
+
+  public void createOrUpdate(UniverseClient client, String token, String buffer, int cursor) {
+    try {
+      CursorArgument cursorArgument = parseCursorArgument(buffer, cursor);
+      if (keywordCompleter == null || keywordCompleter.getCompleter() == null
+          || keywordCompleter.isExpired()) {
+        Set<String> keywords = getKeywordsCompletions();
+        if (keywords != null && !keywords.isEmpty()) {
+          keywordCompleter = new CachedCompleter(new StringsCompleter(keywords), 0);
+        }
+      }
+      if (cursorArgument.needLoadUniverses() || (universeCompleter == null
+          || universeCompleter.getCompleter() == null || universeCompleter.isExpired())) {
+        client.cleanUniverses();
+        client.loadUniverses(token);
+        if (client.getUniversesMap().size() > 0) {
+          universeCompleter = new CachedCompleter(
+              new StringsCompleter(client.getUniversesMap().keySet()), ttlInSeconds);
+        }
+      }
+      if (cursorArgument.needLoadUniverseInfo() &&
+          (!universeInfoCompletersMap.containsKey(cursorArgument.getUniverse()) ||
+              universeInfoCompletersMap.get(cursorArgument.getUniverse()).getCompleter() == null ||
+              universeInfoCompletersMap.get(cursorArgument.getUniverse()).isExpired())) {
+        if (StringUtils.isNotBlank(cursorArgument.getUniverse())) {
+          client.removeUniverseInfo(cursorArgument.getUniverse());
+          Map<String, UniverseNodeInfo> info = client.getUniverseNodesInfo(token, cursorArgument
+              .getUniverse());
+          CachedCompleter completer = new CachedCompleter(
+              new UniverseNodeInfoCompleter(info.values()), ttlInSeconds);
+          universeInfoCompletersMap.put(cursorArgument.getUniverse(), completer);
+        }
+      }
+    } catch (Exception e) {
+      logger.error("Failed to update completions", e);
+    }
+  }
+
+  private Set<String> getKeywordsCompletions() throws IOException {
+    String keywords =
+        new BufferedReader(new InputStreamReader(
+            UniverseCompleter.class.getResourceAsStream("/universe.keywords"))).readLine();
+
+    Set<String> completions = new TreeSet<>();
+
+    if (StringUtils.isNotBlank(keywords)) {
+      String[] words = keywords.split(KEYWORD_SPLITERATOR);
+      for (String word : words) {
+        completions.add(word);
+      }
+    }
+
+    return completions;
+  }
+
+  private CursorArgument parseCursorArgument(String buffer, int cursor) {
+    CursorArgument result = new CursorArgument();
+    if (buffer != null && buffer.length() >= cursor) {
+      String buf = buffer.substring(0, cursor);
+      if (StringUtils.isNotBlank(buf)) {
+        ArgumentList argList = sqlDelimiter.delimit(buf, cursor);
+        int argIndex = argList.getCursorArgumentIndex();
+        if (argIndex == 0) {
+          result.setCursorArgumentPartForComplete(argList.getCursorArgument());
+          return result;
+        }
+
+        if (argIndex > 0 && argList.getArguments()[argIndex - 1].equalsIgnoreCase(KW_UNIVERSE)) {
+          result.setUniverseNamePosition(true);
+          result.setCursorArgumentPartForComplete(cleanName(argList.getCursorArgument()
+              .substring(0, argList.getArgumentPosition())));
+          return result;
+        }
+        if (argIndex > 1) {
+          for (int i = argIndex - 2; i >= 0; i--) {
+            if (argList.getArguments()[i].equalsIgnoreCase(KW_UNIVERSE)) {
+              result.setUniverse(cleanName(argList.getArguments()[i + 1]));
+              break;
+            }
+          }
+
+          if (StringUtils.isNotBlank(result.getUniverse())
+              && argList.getCursorArgument().startsWith(START_NAME.toString())) {
+            result.setCursorArgumentPartForComplete(
+                argList.getCursorArgument().substring(0, argList.getArgumentPosition()));
+            result.setUniverseNodePosition(true);
+            return result;
+          } else {
+            result.setCursorArgumentPartForComplete(argList.getCursorArgument()
+                .substring(0, argList.getArgumentPosition()));
+          }
+        }
+      }
+    }
+
+    if (result.getCursorArgumentPartForComplete() == null) {
+      result.setCursorArgumentPartForComplete(StringUtils.EMPTY);
+    }
+
+    return result;
+  }
+
+  private String cleanName(String name) {
+    return name.replaceAll(CLEAN_NAME_REGEX, StringUtils.EMPTY);
+  }
+
+  private void addCompletions(List<InterpreterCompletion> interpreterCompletions,
+                              List<CharSequence> candidates, String meta) {
+    for (CharSequence candidate : candidates) {
+      String value;
+      if (meta.equalsIgnoreCase(CompletionType.universe.name())) {
+        value = String.format("%s%s;\n", candidate.toString(), END_NAME);
+      } else {
+        value = candidate.toString();
+      }
+      interpreterCompletions.add(new InterpreterCompletion(candidate.toString(), value, meta));
+    }
+  }
+
+  private void addCompletions(List<InterpreterCompletion> interpreterCompletions,
+                              List<UniverseNodeInfo> candidates) {
+    for (UniverseNodeInfo candidate : candidates) {
+      String value;
+      if (candidate.getType().equalsIgnoreCase(TYPE_FOLDER)) {
+        value = String.format("%s%s.%s", candidate.getName(), END_NAME, START_NAME);
+      } else {
+        value = String.format("%s%s", candidate.getName(), END_NAME);
+      }
+      interpreterCompletions.add(new InterpreterCompletion(candidate.getName(), value,
+          candidate.getType()));
+    }
+  }
+
+  public CachedCompleter getUniverseCompleter() {
+    return universeCompleter;
+  }
+
+  public Map<String, CachedCompleter> getUniverseInfoCompletersMap() {
+    return universeInfoCompletersMap;
+  }
+
+  private class CursorArgument {
+    private boolean universeNamePosition = false;
+    private boolean universeNodePosition = false;
+    private String universe;
+    private String cursorArgumentPartForComplete;
+
+    public boolean isUniverseNamePosition() {
+      return universeNamePosition;
+    }
+
+    public void setUniverseNamePosition(boolean universeNamePosition) {
+      this.universeNamePosition = universeNamePosition;
+    }
+
+    public boolean isUniverseNodePosition() {
+      return universeNodePosition;
+    }
+
+    public void setUniverseNodePosition(boolean universeNodePosition) {
+      this.universeNodePosition = universeNodePosition;
+    }
+
+    public String getCursorArgumentPartForComplete() {
+      return cursorArgumentPartForComplete;
+    }
+
+    public void setCursorArgumentPartForComplete(String cursorArgumentPartForComplete) {
+      this.cursorArgumentPartForComplete = cursorArgumentPartForComplete;
+    }
+
+    public String getUniverse() {
+      return universe;
+    }
+
+    public void setUniverse(String universe) {
+      this.universe = universe;
+    }
+
+    public boolean needLoadUniverses() {
+      if (universe == null) {
+        return true;
+      }
+      return false;
+    }
+
+    public boolean needLoadUniverseInfo() {
+      if (universe != null && universeNodePosition) {
+        return true;
+      }
+      return false;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java
new file mode 100644
index 0000000..8086f94
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+
+/**
+ * Runtime Exception for SAP universe
+ */
+public class UniverseException extends Exception {
+
+  public UniverseException(Throwable e) {
+    super(e);
+  }
+
+  public UniverseException(String m) {
+    super(m);
+  }
+
+  public UniverseException(String msg, Throwable t) {
+    super(msg, t);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java
new file mode 100644
index 0000000..4f40dce
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java
@@ -0,0 +1,60 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+/**
+ * Info about of universe node
+ */
+public class UniverseInfo {
+  private String id;
+  private String name;
+  private String type;
+
+  public UniverseInfo() {
+  }
+
+  public UniverseInfo(String id, String name, String type) {
+    this.id = id;
+    this.name = name;
+    this.type = type;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java
new file mode 100644
index 0000000..fe0c97e
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java
@@ -0,0 +1,85 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+/**
+ * Info about of universe item
+ */
+public class UniverseNodeInfo {
+  private String id;
+  private String name;
+  private String type;
+  private String folder;
+  private String nodePath;
+
+  public UniverseNodeInfo() {
+  }
+
+  public UniverseNodeInfo(String id, String name, String type, String folder, String nodePath) {
+    this.id = id;
+    this.name = name;
+    this.type = type;
+    this.folder = folder;
+    this.nodePath = nodePath;
+  }
+
+  public UniverseNodeInfo(String name, String type) {
+    this.name = name;
+    this.type = type;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getFolder() {
+    return folder;
+  }
+
+  public void setFolder(String folder) {
+    this.folder = folder;
+  }
+
+  public String getNodePath() {
+    return nodePath;
+  }
+
+  public void setNodePath(String nodePath) {
+    this.nodePath = nodePath;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java
new file mode 100644
index 0000000..0704e1b
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java
@@ -0,0 +1,183 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import jline.console.completer.Completer;
+import jline.internal.Preconditions;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.*;
+
+/**
+ * Case-insensitive completer.
+ */
+public class UniverseNodeInfoCompleter implements Completer {
+  private final UniverseInfoTreeNode tree = new UniverseInfoTreeNode();
+
+  public UniverseNodeInfoCompleter() {
+  }
+
+  public UniverseNodeInfoCompleter(final Collection<UniverseNodeInfo> nodes) {
+    Preconditions.checkNotNull(nodes);
+    for (UniverseNodeInfo node : nodes) {
+      String folder = node.getFolder();
+      if (StringUtils.isBlank(folder)) {
+        tree.putInfo(node);
+      } else {
+        String[] path = folder.split("\\\\");
+        UniverseInfoTreeNode universeInfoTreeNode = tree;
+        for (String s : path) {
+          if (!universeInfoTreeNode.contains(s)) {
+            universeInfoTreeNode = universeInfoTreeNode.putFolder(s);
+          } else {
+            universeInfoTreeNode = universeInfoTreeNode.getFolder(s);
+          }
+        }
+        universeInfoTreeNode.putInfo(node);
+      }
+    }
+  }
+
+  public int complete(final String buffer, final int cursor, final List candidates) {
+    return completeCollection(buffer, cursor, candidates);
+  }
+
+  private int completeCollection(final String buffer, final int cursor,
+      final Collection candidates) {
+    Preconditions.checkNotNull(candidates);
+    if (buffer == null) {
+      candidates.addAll(tree.getNodesInfo());
+    } else {
+      String part = buffer.substring(0, cursor);
+      List<String> path = new ArrayList<>();
+      path.addAll(Arrays.asList(part.split("\\]\\.\\[")));
+      if (part.endsWith(UniverseCompleter.START_NAME.toString())) {
+        path.add(StringUtils.EMPTY);
+      }
+
+      UniverseInfoTreeNode treeNode = tree;
+      for (int i = 0; i < path.size() - 1; i++) {
+        String folder = cleanName(path.get(i));
+        if (treeNode.contains(folder)) {
+          treeNode = treeNode.getFolder(folder);
+          if (treeNode == null) {
+            break;
+          }
+        }
+      }
+      String p = cleanName(path.get(path.size() - 1)).toUpperCase();
+      if (treeNode != null && treeNode.getChildren() != null) {
+        if (p.isEmpty()) {
+          candidates.addAll(treeNode.getNodesInfo());
+        } else {
+          for (UniverseNodeInfo universeNodeInfo : treeNode.getNodesInfo()) {
+            if (universeNodeInfo.getName().toUpperCase().startsWith(p)) {
+              candidates.add(universeNodeInfo);
+            }
+          }
+        }
+      }
+    }
+
+    return candidates.isEmpty() ? -1 : 0;
+  }
+
+  private String cleanName(String name) {
+    return name.replaceAll(UniverseCompleter.CLEAN_NAME_REGEX, StringUtils.EMPTY);
+  }
+
+  private class UniverseInfoTreeNode {
+    private String name;
+    private boolean isFolder;
+    private Map<String, Object> children;
+
+    public UniverseInfoTreeNode() {
+      this.name = "/";
+      this.isFolder = true;
+      this.children = new HashMap<>();
+    }
+
+    public UniverseInfoTreeNode(String name) {
+      this.name = name;
+      this.isFolder = true;
+      this.children = new HashMap<>();
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public boolean isFolder() {
+      return isFolder;
+    }
+
+    public void setFolder(boolean folder) {
+      isFolder = folder;
+    }
+
+    public Map<String, Object> getChildren() {
+      return children;
+    }
+
+    public void setChildren(Map<String, Object> children) {
+      this.children = children;
+    }
+
+    public boolean contains(String name) {
+      return children.containsKey(name);
+    }
+
+    public UniverseInfoTreeNode getFolder(String name) {
+      Object child = children.get(name);
+      if (child instanceof UniverseInfoTreeNode) {
+        return (UniverseInfoTreeNode) children.get(name);
+      }
+
+      return null;
+    }
+
+    public UniverseInfoTreeNode putFolder(String name) {
+      UniverseInfoTreeNode newNode = new UniverseInfoTreeNode(name);
+      children.put(name, newNode);
+      return newNode;
+    }
+
+    public void putInfo(UniverseNodeInfo info) {
+      children.put(info.getName(), info);
+    }
+
+    public List<UniverseNodeInfo> getNodesInfo() {
+      List<UniverseNodeInfo> list = new ArrayList<>();
+      if (children != null) {
+        for (Object o : children.values()) {
+          if (o instanceof UniverseNodeInfo) {
+            list.add((UniverseNodeInfo) o);
+          } else {
+            UniverseInfoTreeNode treeNode = (UniverseInfoTreeNode) o;
+            list.add(new UniverseNodeInfo(treeNode.getName(), UniverseCompleter.TYPE_FOLDER));
+          }
+        }
+      }
+
+      return list;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java
new file mode 100644
index 0000000..824d1be
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java
@@ -0,0 +1,53 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Data of universe query
+ */
+public class UniverseQuery {
+  private String select;
+  private String where;
+  private UniverseInfo universeInfo;
+
+  public UniverseQuery(String select, String where, UniverseInfo universeInfo) {
+    this.select = select;
+    this.where = where;
+    this.universeInfo = universeInfo;
+  }
+
+  public boolean isCorrect() {
+    return StringUtils.isNotBlank(select) && universeInfo != null &&
+        StringUtils.isNotBlank(universeInfo.getId())
+        && StringUtils.isNotBlank(universeInfo.getName());
+  }
+
+  public String getSelect() {
+    return select;
+  }
+
+  public String getWhere() {
+    return where;
+  }
+
+  public UniverseInfo getUniverseInfo() {
+    return universeInfo;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/63c53fcc/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java
----------------------------------------------------------------------
diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java
new file mode 100644
index 0000000..04b2b49
--- /dev/null
+++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java
@@ -0,0 +1,110 @@
+/*
+ * 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.zeppelin.sap.universe;
+
+/**
+ * Info about parameter of universe query
+ */
+public class UniverseQueryPrompt {
+  private Integer id;
+  private String name;
+  private String cardinality;
+  private String constrained;
+  private String type;
+  private String value;
+  private String technicalName;
+  private String keepLastValues;
+
+  public UniverseQueryPrompt() {
+  }
+
+  public UniverseQueryPrompt(Integer id, String name, String cardinality, String constrained,
+                             String type, String technicalName, String keepLastValues) {
+    this.id = id;
+    this.name = name;
+    this.cardinality = cardinality;
+    this.constrained = constrained;
+    this.type = type;
+    this.technicalName = technicalName;
+    this.keepLastValues = keepLastValues;
+  }
+
+  public Integer getId() {
+    return id;
+  }
+
+  public void setId(Integer id) {
+    this.id = id;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getCardinality() {
+    return cardinality;
+  }
+
+  public void setCardinality(String cardinality) {
+    this.cardinality = cardinality;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  public String getConstrained() {
+    return constrained;
+  }
+
+  public void setConstrained(String constrained) {
+    this.constrained = constrained;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getTechnicalName() {
+    return technicalName;
+  }
+
+  public void setTechnicalName(String technicalName) {
+    this.technicalName = technicalName;
+  }
+
+  public String getKeepLastValues() {
+    return keepLastValues;
+  }
+
+  public void setKeepLastValues(String keepLastValues) {
+    this.keepLastValues = keepLastValues;
+  }
+}