diff --git a/geode/pom.xml b/geode/pom.xml index 3fa7cb3ddd6..a3ce21127e4 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -51,6 +51,17 @@ ${geode.version} + + com.google.guava + guava + + + + jline + jline + 2.12.1 + + org.apache.commons commons-exec diff --git a/geode/src/main/java/org/apache/zeppelin/geode/GeodeOqlInterpreter.java b/geode/src/main/java/org/apache/zeppelin/geode/GeodeOqlInterpreter.java index 6f6b440830c..f0d746c28fa 100644 --- a/geode/src/main/java/org/apache/zeppelin/geode/GeodeOqlInterpreter.java +++ b/geode/src/main/java/org/apache/zeppelin/geode/GeodeOqlInterpreter.java @@ -14,6 +14,7 @@ */ package org.apache.zeppelin.geode; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -35,6 +36,8 @@ import com.gemstone.gemfire.cache.query.SelectResults; import com.gemstone.gemfire.cache.query.Struct; import com.gemstone.gemfire.pdx.PdxInstance; +import com.google.common.base.Function; +import com.google.common.collect.Lists; /** * Apache Geode OQL Interpreter (http://geode.incubator.apache.org) @@ -77,6 +80,9 @@ *} *

*

+ * Use (Ctrl + .) to activate the auto-completion. + *

+ *

* Known issue:http://gemfire.docs.pivotal.io/bugnotes/KnownIssuesGemFire810.html #43673 Using query * "select * from /exampleRegion.entrySet" fails in a client-server topology and/or in a * PartitionedRegion. @@ -99,6 +105,7 @@ public class GeodeOqlInterpreter extends Interpreter { public static final String LOCATOR_HOST = "geode.locator.host"; public static final String LOCATOR_PORT = "geode.locator.port"; public static final String MAX_RESULT = "geode.max.result"; + private static final List NO_COMPLETION = new ArrayList(); static { Interpreter.register( @@ -110,11 +117,20 @@ public class GeodeOqlInterpreter extends Interpreter { .add(MAX_RESULT, DEFAULT_MAX_RESULT, "Max number of OQL result to display.").build()); } + private static final Function sequenceToStringTransformer = + new Function() { + public String apply(CharSequence seq) { + return seq.toString(); + } + }; + private ClientCache clientCache = null; private QueryService queryService = null; private Exception exceptionOnConnect; private int maxResult; + private OqlCompleter oqlCompleter; + public GeodeOqlInterpreter(Properties property) { super(property); } @@ -142,6 +158,7 @@ public void open() { clientCache = getClientCache(); queryService = clientCache.getQueryService(); + oqlCompleter = new OqlCompleter(OqlCompleter.getOqlCompleterTokens(clientCache)); exceptionOnConnect = null; logger.info("Successfully created Geode connection"); @@ -301,7 +318,12 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor) { - return null; + List candidates = new ArrayList(); + if (oqlCompleter.complete(buf, cursor, candidates) >= 0) { + return Lists.transform(candidates, sequenceToStringTransformer); + } else { + return NO_COMPLETION; + } } public int getMaxResult() { diff --git a/geode/src/main/java/org/apache/zeppelin/geode/OqlCompleter.java b/geode/src/main/java/org/apache/zeppelin/geode/OqlCompleter.java new file mode 100644 index 00000000000..4265c91ecda --- /dev/null +++ b/geode/src/main/java/org/apache/zeppelin/geode/OqlCompleter.java @@ -0,0 +1,100 @@ +/** + * 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.geode; + +import static org.apache.commons.lang.StringUtils.isBlank; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +import jline.console.completer.ArgumentCompleter.ArgumentList; +import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter; +import jline.console.completer.StringsCompleter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gemstone.gemfire.cache.client.ClientCache; + +/** + * OQL auto-completer for the {@link GeodeOqlInterpreter}. + */ +public class OqlCompleter extends StringsCompleter { + + private static Logger logger = LoggerFactory.getLogger(OqlCompleter.class); + + private WhitespaceArgumentDelimiter delimiter = new WhitespaceArgumentDelimiter(); + + public OqlCompleter(Set completions) { + super(completions); + } + + @Override + public int complete(String buffer, int cursor, List candidates) { + + if (isBlank(buffer) || cursor > buffer.length() + 1) { + return -1; + } + + // The delimiter breaks the buffer into separate words (arguments), separated by the + // whitespaces. + ArgumentList argumentList = delimiter.delimit(buffer, cursor); + String argument = argumentList.getCursorArgument(); + // cursor in the selected argument + int argumentPosition = argumentList.getArgumentPosition(); + + if (isBlank(argument)) { + int argumentsCount = argumentList.getArguments().length; + if (argumentsCount <= 0 || ((buffer.length() + 2) < cursor) + || delimiter.isDelimiterChar(buffer, cursor - 2)) { + return -1; + } + argument = argumentList.getArguments()[argumentsCount - 1]; + argumentPosition = argument.length(); + } + + int complete = super.complete(argument, argumentPosition, candidates); + + logger.debug("complete:" + complete + ", size:" + candidates.size()); + + return complete; + } + + public static Set getOqlCompleterTokens(ClientCache cache) throws IOException { + + Set completions = new TreeSet(); + + // add the default OQL completions + String keywords = + new BufferedReader(new InputStreamReader( + OqlCompleter.class.getResourceAsStream("/oql.keywords"))).readLine(); + + + // Also allow upper-case versions of all the keywords + keywords += "," + keywords.toUpperCase(); + + StringTokenizer tok = new StringTokenizer(keywords, ","); + while (tok.hasMoreTokens()) { + completions.add(tok.nextToken()); + } + + return completions; + } +} diff --git a/geode/src/main/resources/oql.keywords b/geode/src/main/resources/oql.keywords new file mode 100644 index 00000000000..809c4df530c --- /dev/null +++ b/geode/src/main/resources/oql.keywords @@ -0,0 +1 @@ +all,and,array,as,asc,boolean,by,byte,char,collection,count,date,desc,dictionary,distinct,double,element,false,float,from,import,in,int,is_defined,is_undefined,like,limit,long,map,nil,not,null,nvl,octet,or,order,select,set,short,string,time,timestamp,to_date,true,type,undefined,where,abs,any,andthen,avg,bag,declare,define,enum,except,exists,for,first,flatten,group,having,intersect,interval,last,list,listtoset,max,min,mod,orelse,query,some,struct,sum,undefine,union,unique diff --git a/geode/src/test/java/org/apache/zeppelin/geode/GeodeOqlInterpreterTest.java b/geode/src/test/java/org/apache/zeppelin/geode/GeodeOqlInterpreterTest.java index 78755eba5a4..7b3fcdf6df0 100644 --- a/geode/src/test/java/org/apache/zeppelin/geode/GeodeOqlInterpreterTest.java +++ b/geode/src/test/java/org/apache/zeppelin/geode/GeodeOqlInterpreterTest.java @@ -15,7 +15,6 @@ package org.apache.zeppelin.geode; import static org.apache.zeppelin.geode.GeodeOqlInterpreter.*; - import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; @@ -27,6 +26,7 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -178,4 +178,20 @@ public void oqlWithExceptionOnConnect() throws Exception { public void testFormType() { assertEquals(FormType.SIMPLE, new GeodeOqlInterpreter(new Properties()).getFormType()); } + + @Test + public void testAutoCompletion() throws SQLException { + Properties properties = new Properties(); + properties.put(LOCATOR_HOST, DEFAULT_HOST); + properties.put(LOCATOR_PORT, DEFAULT_PORT); + properties.put(MAX_RESULT, DEFAULT_MAX_RESULT); + + GeodeOqlInterpreter spyGeodeOqlInterpreter = spy(new GeodeOqlInterpreter(properties)); + + spyGeodeOqlInterpreter.open(); + + assertEquals(1, spyGeodeOqlInterpreter.completion("SEL", 0).size()); + assertEquals("SELECT ", spyGeodeOqlInterpreter.completion("SEL", 0).iterator().next()); + assertEquals(0, spyGeodeOqlInterpreter.completion("SEL", 100).size()); + } } diff --git a/geode/src/test/java/org/apache/zeppelin/geode/OqlCompleterTest.java b/geode/src/test/java/org/apache/zeppelin/geode/OqlCompleterTest.java new file mode 100644 index 00000000000..2dbffdec467 --- /dev/null +++ b/geode/src/test/java/org/apache/zeppelin/geode/OqlCompleterTest.java @@ -0,0 +1,158 @@ +/** + * 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.geode; + +import static com.google.common.collect.Sets.newHashSet; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import jline.console.completer.Completer; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Joiner; + +public class OqlCompleterTest { + + private Logger logger = LoggerFactory.getLogger(OqlCompleterTest.class); + + private final static Set EMPTY = new HashSet(); + + private CompleterTester tester; + + @Before + public void beforeTest() throws IOException, SQLException { + OqlCompleter sqlCompleter = new OqlCompleter(OqlCompleter.getOqlCompleterTokens(null)); + tester = new CompleterTester(sqlCompleter); + } + + @Test + public void testEdges() { + String buffer = " ORDER "; + tester.buffer(buffer).from(0).to(8).expect(newHashSet("ORDER ")).test(); + tester.buffer(buffer).from(9).to(15).expect(EMPTY).test(); + } + + @Test + public void testMultipleWords() { + String buffer = " SELE fro"; + tester.buffer(buffer).from(0).to(6).expect(newHashSet("SELECT ")).test(); + tester.buffer(buffer).from(7).to(12).expect(newHashSet("from ")).test(); + tester.buffer(buffer).from(13).to(14).expect(EMPTY).test(); + } + + @Test + public void testMultiLineBuffer() { + String buffer = " \n SELE \n fro"; + tester.buffer(buffer).from(0).to(7).expect(newHashSet("SELECT ")).test(); + tester.buffer(buffer).from(8).to(14).expect(newHashSet("from ")).test(); + tester.buffer(buffer).from(15).to(16).expect(EMPTY).test(); + } + + @Test + public void testMultipleCompletionSuggestions() { + String buffer = " S"; + tester.buffer(buffer).from(0).to(4) + .expect(newHashSet("STRUCT", "SHORT", "SET", "SUM", "SELECT", "SOME", "STRING")).test(); + tester.buffer(buffer).from(5).to(7).expect(EMPTY).test(); + } + + public class CompleterTester { + + private Completer completer; + + private String buffer; + private int fromCursor; + private int toCursor; + private Set expectedCompletions; + + public CompleterTester(Completer completer) { + this.completer = completer; + } + + public CompleterTester buffer(String buffer) { + this.buffer = buffer; + return this; + } + + public CompleterTester from(int fromCursor) { + this.fromCursor = fromCursor; + return this; + } + + public CompleterTester to(int toCursor) { + this.toCursor = toCursor; + return this; + } + + public CompleterTester expect(Set expectedCompletions) { + this.expectedCompletions = expectedCompletions; + return this; + } + + public void test() { + for (int c = fromCursor; c <= toCursor; c++) { + expectedCompletions(buffer, c, expectedCompletions); + } + } + + private void expectedCompletions(String buffer, int cursor, Set expected) { + + ArrayList candidates = new ArrayList(); + + completer.complete(buffer, cursor, candidates); + + String explain = explain(buffer, cursor, candidates); + + logger.info(explain); + + assertEquals("Buffer [" + buffer.replace(" ", ".") + "] and Cursor[" + cursor + "] " + + explain, expected, newHashSet(candidates)); + } + + private String explain(String buffer, int cursor, ArrayList candidates) { + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i <= Math.max(cursor, buffer.length()); i++) { + if (i == cursor) { + sb.append("("); + } + if (i >= buffer.length()) { + sb.append("_"); + } else { + if (Character.isWhitespace(buffer.charAt(i))) { + sb.append("."); + } else { + sb.append(buffer.charAt(i)); + } + } + if (i == cursor) { + sb.append(")"); + } + } + sb.append(" >> [").append(Joiner.on(",").join(candidates)).append("]"); + + return sb.toString(); + } + } +}