/*
* Licensed to Luca Cavanna (the "Author") under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.elasticsearch.shell.console.completer;
import jline.console.completer.Completer;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.shell.RhinoShellTopLevel;
import org.elasticsearch.shell.ShellScope;
import org.mozilla.javascript.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* JLine completer based on the Rhino engine and its top-level object (scope)
* Looks within the Rhino scope to find available objects.
* Uses reflection to find suggestions given the return type of a method (fluent interface)
* In case of methods with different signatures (same name but different return types) all the possible matches
* will be merged and provided as suggestions
*
* It doesn't support auto-suggestions for array elements
* It doesn't complete packages with contained classes
*
* @author Luca Cavanna
*/
public class JLineRhinoCompleter implements Completer {
private static final Logger logger = LoggerFactory.getLogger(JLineRhinoCompleter.class);
private final ShellScope<RhinoShellTopLevel> shellScope;
private final List<String> excludeList = new ArrayList<String>();
private final IdentifierTokenizer idTokenizer = new IdentifierTokenizer();
@Inject
JLineRhinoCompleter(ShellScope<RhinoShellTopLevel> shellScope) {
this.shellScope = shellScope;
for (Method method : Object.class.getMethods()) {
if (!"toString".equals(method.getName())
&& !"equals".equals(method.getName())
&& !"getClass".equals(method.getName())) {
this.excludeList.add(method.getName());
}
}
this.excludeList.add("class");
}
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
try {
return tryComplete(buffer, cursor, candidates);
} catch(Exception e) {
logger.warn("Exception while trying to complete buffer {}, cursor {}", buffer, cursor,e);
return buffer.length();
}
}
public int tryComplete(String buffer, int cursor, List<CharSequence> candidates) {
logger.debug("Trying to complete buffer [{}], cursor {}", buffer, cursor);
List<Identifier> identifiers = idTokenizer.tokenize(buffer, cursor);
logger.debug("Identifiers: {}", identifiers);
//looks for the last object whose name is complete
Scriptable object = this.shellScope.get();
Set<Class<?>> returnTypes = null;
boolean newFound = false;
for (int i = 0; i < identifiers.size() - 1; i++) {
String currentName = identifiers.get(i).getName();
//when we get to the new keyword we just go ahead with the next name (we'll take into account later)
if ("new".equals(currentName)) {
newFound = true;
continue;
}
Object val;
try {
val = object.get(currentName, this.shellScope.get());
logger.debug("Found {} while looking for [{}] in {}", val.getClass(), currentName, object.getClass());
if (newFound && !(val instanceof NativeJavaPackage) && !(val instanceof NativeJavaClass)) {
newFound = false;
}
} catch(EvaluatorException e) {
logger.debug("Error while looking for [{}] in {}", currentName, object.getClass());
return getLastPartStartPosition(identifiers); // no matches
}
if (val instanceof RhinoCustomNativeJavaClass && newFound) {
Class<?> clazz = ((RhinoCustomNativeJavaClass) val).getClazz();
returnTypes = new HashSet<Class<?>>();
returnTypes.add(clazz);
for (int j = i + 1; j < identifiers.size() - 1; j++) {
if (returnTypes.isEmpty()) {
return getLastPartStartPosition(identifiers); // no matches
}
//gets all the possible return types given the input types
returnTypes = findReturnTypes(returnTypes, identifiers.get(j).getName());
}
break;
}
//If we have a java method we won't find it within the parent object
//we need to lookup the remaining elements through reflections
if (val instanceof RhinoCustomNativeJavaMethod) {
RhinoCustomNativeJavaMethod nativeJavaMethod = (RhinoCustomNativeJavaMethod) val;
//gets the return types given the native java method
returnTypes = nativeJavaMethod.getReturnTypes();
for (int j = i + 1; j < identifiers.size() - 1; j++) {
if (returnTypes.isEmpty()) {
return getLastPartStartPosition(identifiers); // no matches
}
//gets all the possible return types given the input types
returnTypes = findReturnTypes(returnTypes, identifiers.get(j).getName());
}
break;
}
if (!(val instanceof Scriptable)) {
if (object.getPrototype() == null) {
return getLastPartStartPosition(identifiers); // no matches
}
val = object.getPrototype().get(currentName, this.shellScope.get());
if (!(val instanceof Scriptable)) {
return getLastPartStartPosition(identifiers); // no matches
}
}
object = (Scriptable)val;
}
Identifier lastPart = identifiers.get(identifiers.size() - 1);
logger.debug("LastPart: {}", lastPart);
if (returnTypes == null) {
findCandidatesInScriptable(object, lastPart.getName(), candidates);
} else {
findCandidatesInReturnTypes(returnTypes, lastPart.getName(), candidates);
}
return lastPart.getFirstPosition();
}
private int getLastPartStartPosition(List<Identifier> identifiers) {
return identifiers.get(identifiers.size() - 1).getFirstPosition();
}
private void findCandidatesInScriptable(Scriptable object, String prefix, List<CharSequence> candidates) {
//Gets the candidates related to the current context (last object whose name is complete)
Object[] ids = object instanceof ScriptableObject ? ((ScriptableObject) object).getAllIds() : object.getIds();
if (ids != null) {
if (logger.isDebugEnabled()) {
logger.debug("Ids: {}", Arrays.asList(ids));
}
addCandidatesFromScriptable(ids, object, prefix, candidates);
logger.debug("Candidates after 1st round: {}", candidates);
}
if (object.getPrototype() != null && object.getPrototype().getIds() != null) {
if (logger.isDebugEnabled()) {
logger.debug("Prototype ids: {}", Arrays.asList(object.getPrototype().getIds()));
}
addCandidatesFromScriptable(object.getPrototype().getIds(), object.getPrototype(), prefix, candidates);
logger.debug("Candidates after 2nd round (object prototype): {}", candidates);
}
}
private void addCandidatesFromScriptable(Object[] ids, Scriptable parent, String lastPart, List<CharSequence> candidates){
for (Object idObject : ids) {
if (idObject instanceof String) {
String id = (String) idObject;
if (id.startsWith(lastPart) && !excludeList.contains(id)) {
Object object = parent.get(id, parent);
if (!(object instanceof UniqueTag)) {
if (object instanceof Function && !(object instanceof NativeJavaClass)) {
id += "()";
}
candidates.add(id);
}
}
}
}
}
private Set<Class<?>> findReturnTypes(Set<Class<?>> classes, String methodName) {
Set<Class<?>> returnTypes = new HashSet<Class<?>>();
for (Class<?> clazz : classes) {
returnTypes.addAll(findReturnTypes(clazz, methodName));
}
return returnTypes;
}
private Set<Class<?>> findReturnTypes(Class<?> clazz, String methodName) {
Set<Class<?>> returnTypes = new HashSet<Class<?>>();
for (Method method : clazz.getMethods()) {
if (method.getName().equals(methodName)) {
returnTypes.add(method.getReturnType());
}
}
return returnTypes;
}
private void findCandidatesInReturnTypes(Set<Class<?>> returnTypes, String prefix, List<CharSequence> candidates) {
for (Class<?> clazz : returnTypes) {
candidates.addAll(findCandidates(clazz, prefix));
}
}
private Set<String> findCandidates(Class<?> clazz, String methodPrefix) {
Set<String> returnTypes = new HashSet<String>();
for (Method method : clazz.getMethods()) {
if (method.getName().startsWith(methodPrefix) && !excludeList.contains(method.getName())) {
returnTypes.add(method.getName() + "()");
}
}
for (Field field : clazz.getFields()) {
if (field.getName().startsWith(methodPrefix) && !excludeList.contains(field.getName())) {
returnTypes.add(field.getName());
}
}
return returnTypes;
}
ShellScope<RhinoShellTopLevel> getScope() {
return shellScope;
}
}