/*
* RHQ Management Platform
* Copyright (C) 2005-2012 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.scripting.javascript;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.script.Bindings;
import javax.script.ScriptContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.ScriptableObject;
import org.rhq.scripting.CodeCompletion;
import org.rhq.scripting.MetadataProvider;
/**
* A Contextual JavaScript interactive completor. Not perfect, but
* handles a fair number of cases.
*
* @author Greg Hinkle
* @author Lukas Krejci
*/
public class JavascriptCompletor implements CodeCompletion {
private static final Log LOG = LogFactory.getLog(JavascriptCompletor.class);
private ScriptContext context;
private MetadataProvider metadataProvider;
private String lastComplete;
// Consecutive times this exact complete has been requested
private int recomplete;
private static final Set<String> IGNORED_METHODS;
static {
IGNORED_METHODS = new HashSet<String>();
IGNORED_METHODS.add("newProxyInstance");
IGNORED_METHODS.add("hashCode");
IGNORED_METHODS.add("equals");
IGNORED_METHODS.add("getInvocationHandler");
IGNORED_METHODS.add("setHandler");
IGNORED_METHODS.add("isProxyClass");
IGNORED_METHODS.add("newProxyInstance");
IGNORED_METHODS.add("getProxyClass");
IGNORED_METHODS.add("main");
IGNORED_METHODS.add("handler");
IGNORED_METHODS.add("init");
IGNORED_METHODS.add("initChildren");
IGNORED_METHODS.add("initMeasurements");
IGNORED_METHODS.add("initOperations");
}
@Override
@SuppressWarnings("unchecked")
public int complete(PrintWriter output, String s, int i, @SuppressWarnings("rawtypes") List list) {
try {
if (lastComplete != null && lastComplete.equals(s)) {
recomplete++;
} else {
recomplete = 1;
}
lastComplete = s;
String base = s;
int rootLength = 0;
if (s.indexOf('=') > 0) {
base = s.substring(s.indexOf("=") + 1).trim();
rootLength = s.length() - base.length();
}
String[] call = base.split("\\.");
if (base.endsWith(".")) {
String[] argPadded = new String[call.length + 1];
System.arraycopy(call, 0, argPadded, 0, call.length);
argPadded[call.length] = "";
call = argPadded;
}
if (call.length == 1) {
Map<String, Object> matches = getContextMatches(call[0]);
if (matches.size() == 1 && matches.containsKey(call[0]) && !s.endsWith(".")) {
list.add(".");
return rootLength + call[0].length() + 1;
} else {
list.addAll(matches.keySet());
}
} else {
Object rootObject = context.getAttribute(call[0]);
if (rootObject != null) {
String theRest = base.substring(call[0].length() + 1, base.length());
int matchIndex = contextComplete(output, rootObject, theRest, i, list);
Collections.sort(list);
return rootLength + call[0].length() + 1 + matchIndex;
}
}
Collections.sort(list);
return (list.size() == 0) ? (-1) : rootLength;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
@Override
public void setMetadataProvider(MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
}
/**
* Base Object can be an object where we're looking for methods on it, or an
* interface. This recursively works off the completions left to right.
*
* Objects can be completed with fields or method calls.
* method parameters are completed with type matching
* method result chainings are completed based on declared return types
*
* e.g. have a Resource in context as myResource. Original string is
* "myResource.name". This method would be called with a baseObject ==
* to myResource and the string "name".
*
* Note: this method will not and should not execute methods, but will
* read field properties to continue chained completions.
*
* @param output the output that can the completor use to convey info to the user
* @param baseObject the context object or class to complete from
* @param s the relative command string to check
* @param i
* @param list
* @return location of relative completion
*/
private int contextComplete(PrintWriter output, Object baseObject, String s, int i, List<String> list) {
if (s.contains(".")) {
String[] call = s.split("\\.", 2);
String next = call[0];
if (next.contains("(")) {
next = next.substring(0, next.indexOf("("));
}
Map<String, List<Object>> matches = getContextMatches(output, baseObject, next);
if (!matches.isEmpty()) {
// BZ 871407 NPE on auto completion for javascript object
List<Object> nextList = matches.get(next);
if (nextList == null || nextList.isEmpty()) {
return -1;
}
Object rootObject = nextList.get(0);
if (rootObject instanceof PropertyDescriptor && !(baseObject instanceof Class)) {
try {
Method readMethod = ((PropertyDescriptor) rootObject).getReadMethod();
//the read method might be null for for example indexed bean properties.
//Rhino doesn't interpret any more complex bean properties.
if (readMethod != null) {
rootObject = invoke(baseObject, readMethod);
} else {
return -1;
}
} catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Exception while reading a java bean property.", e);
}
return -1;
}
} else if (rootObject instanceof Method) {
rootObject = ((Method) rootObject).getReturnType();
}
return call[0].length() + 1 + contextComplete(output, rootObject, call[1], i, list);
} else {
return -1;
}
} else {
String[] call = s.split("\\(", 2);
Map<String, List<Object>> matches = getContextMatches(output, baseObject, call[0]);
if (call.length == 2 && matches.containsKey(call[0])) {
int x = 0;
for (String key : matches.keySet()) {
List<Object> matchList = matches.get(key);
if (recomplete == 2) {
List<Method> methods = new ArrayList<Method>();
for (Object match : matchList) {
if (match instanceof Method) {
methods.add((Method) match);
}
}
displaySignatures(output, baseObject, methods.toArray(new Method[methods.size()]));
return -1;
}
for (Object match : matchList) {
if (key.equals(call[0]) && match instanceof Method) {
int result = completeParameters(baseObject, call[1], i, list, (Method) match); // x should be the same for all calls
if (result > 0) {
x = result;
}
}
}
}
return call[0].length() + 1 + x;
}
if (matches.size() == 1 && matches.containsKey(call[0])) {
Object obj = matches.get(call[0]).get(0);
if (isMethod(obj)) {
boolean close = obj instanceof Method && ((Method) obj).getParameterTypes().length == 0;
list.add("(" + (close ? ")" : ""));
}
return call[0].length() + 1;
}
if (recomplete == 2) {
List<Method> methods = new ArrayList<Method>();
for (List<Object> matchList : matches.values()) {
for (Object val : matchList) {
if (val instanceof Method) {
methods.add((Method) val);
}
}
}
displaySignatures(output, baseObject, methods.toArray(new Method[methods.size()]));
} else {
if (matches.size() == 1 && matches.values().iterator().next().get(0) instanceof Method) {
list.add(matches.keySet().iterator().next()
+ "("
+ ((((Method) matches.values().iterator().next().get(0)).getParameterTypes().length == 0 ? ")"
: "")));
} else {
list.addAll(matches.keySet());
}
}
return 0;
}
}
private void displaySignatures(PrintWriter output, Object object, Method... methods) {
try {
String[][] signatures = new String[methods.length][];
int i = 0;
for (Method m : methods) {
signatures[i++] = getSignature(object, m).split(" ", 2);
}
int maxReturnLength = 0;
for (String[] sig : signatures) {
if (sig[0].length() > maxReturnLength)
maxReturnLength = sig[0].length();
}
output.println();
output.println();
for (String[] sig : signatures) {
for (i = 0; i < (maxReturnLength - sig[0].length()); i++) {
output.print(" ");
}
output.print(sig[0]);
output.print(" ");
output.print(sig[1]);
output.println();
}
} catch (Exception e) {
e.printStackTrace(output);
}
}
/**
* Split apart the parameters to a method call and complete the last parameter. If the last
* paramater has a valid value close that field with a "," for the next param or a ")" if is
* the last parameter. Does all machting according to the type of the parameters of the
* supplied method.
*
* @param baseObject
* @param params
* @param i
* @param list
* @param method
* @return
*/
public int completeParameters(Object baseObject, String params, int i, List<String> list, Method method) {
String[] paramList = params.split(",");
Class<?>[] c = method.getParameterTypes();
String lastParam = paramList[paramList.length - 1];
int paramIndex = paramList.length - 1;
if (params.trim().endsWith(",")) {
lastParam = "";
paramIndex++;
}
int baseLength = 0;
for (int x = 0; x < paramIndex; x++) {
Object paramFound = context.getAttribute(paramList[x]);
if (paramFound != null && !c[x].isAssignableFrom(paramFound.getClass())) {
return -1;
}
baseLength += paramList[x].length() + 1;
}
if (paramIndex >= c.length) {
if (params.endsWith(")")) {
return -1;
} else {
list.add(")");
return (params + ")").length();
}
} else {
if (baseObject instanceof Map && method.getName().equals("get") && method.getParameterTypes().length == 1) {
//unused Class<?> keyType = method.getParameterTypes()[0];
for (Object key : ((Map<?, ?>) baseObject).keySet()) {
String lookupChoice = "\'" + String.valueOf(key) + "\'";
if (lookupChoice.startsWith(lastParam)) {
list.add(lookupChoice);
}
}
if (list.size() == 1) {
list.set(0, list.get(0) + ")");
}
} else {
Class<?> parameterType = c[paramIndex];
Map<String, Object> matches = getContextMatches(lastParam, parameterType);
if (matches.size() == 1 && matches.containsKey(lastParam)) {
list.add(paramIndex == c.length - 1 ? ")" : ",");
return baseLength + lastParam.length();
} else {
list.addAll(matches.keySet());
}
}
return baseLength;
}
}
private Map<String, Object> getContextMatches(String start) {
Map<String, Object> found = new HashMap<String, Object>();
if (context != null) {
for (Integer scope : context.getScopes()) {
Bindings bindings = context.getBindings(scope);
for (String var : bindings.keySet()) {
if (var.startsWith(start)) {
found.put(var, bindings.get(var));
}
}
}
}
//this was originally part of the code completor that lived in the CLI
//I don't think we need it, because the services are present under the
//same names in the context. This code can never add any new matches.
/*
if (services != null) {
for (String var : services.keySet()) {
if (var.startsWith(start)) {
found.put(var, services.get(var));
}
}
}
*/
return found;
}
/**
* Look through all available contexts to find bindings that both start with
* the supplied start and match the typeFilter.
* @param start
* @param typeFilter
* @return
*/
private Map<String, Object> getContextMatches(String start, Class<?> typeFilter) {
Map<String, Object> found = new HashMap<String, Object>();
if (context != null) {
for (int scope : context.getScopes()) {
Bindings bindings = context.getBindings(scope);
for (String var : bindings.keySet()) {
if (var.startsWith(start)) {
if ((bindings.get(var) != null && typeFilter.isAssignableFrom(bindings.get(var).getClass()))
|| recomplete == 3) {
found.put(var, bindings.get(var));
}
}
}
}
if (typeFilter.isEnum()) {
for (Object ec : typeFilter.getEnumConstants()) {
Enum<?> e = (Enum<?>) ec;
String code = typeFilter.getSimpleName() + "." + e.name();
if (code.startsWith(start)) {
found.put(typeFilter.getSimpleName() + "." + e.name(), e);
}
}
}
}
return found;
}
private Map<String, List<Object>> getContextMatches(PrintWriter output, Object baseObject, String start) {
Map<String, List<Object>> found = new HashMap<String, List<Object>>();
if (baseObject != null) {
Class<?> baseObjectClass = null;
if (baseObject instanceof Class) {
baseObjectClass = (Class<?>) baseObject;
} else {
baseObjectClass = baseObject.getClass();
}
try {
if (baseObjectClass.equals(Void.TYPE)) {
return found;
} else if (ScriptableObject.class.isAssignableFrom(baseObjectClass)) {
return findJavascriptContextMatches((ScriptableObject) baseObject, start);
} else {
return findJavaBeanContextMatches(baseObject, baseObjectClass, start);
}
} catch (Exception e) {
LOG.info("Failure during code completion", e);
e.printStackTrace(output);
}
}
return found;
}
private Map<String, List<Object>> findJavascriptContextMatches(ScriptableObject object, String start) {
// don't attempt ID completion on arrays.. While this would return the available indices in the array which
// is a very useful completion hint, we currently only support code completion on dots. I.e. if "a" was an
// array, code completion on "a." would return the indices of the array. The user could then be tempted to
// to use such completed constructs even though they're not a valid javascript ("a.0" is not a valid
// javascript expression).
if (object instanceof NativeArray) {
return Collections.emptyMap();
}
HashMap<String, List<Object>> ret = new HashMap<String, List<Object>>();
for (Object o : object.getIds()) {
String key = o.toString();
if (start == null || start.isEmpty() || key.startsWith(start)) {
Object target = object.get(key);
ret.put(key, new ArrayList<Object>(Arrays.asList(target)));
}
}
return ret;
}
private Map<String, List<Object>> findJavaBeanContextMatches(Object baseObject, Class<?> baseObjectClass,
String start) throws IntrospectionException {
Map<String, List<Object>> found = new HashMap<String, List<Object>>();
BeanInfo info = null;
if (baseObjectClass.isInterface() || baseObjectClass.equals(Object.class)) {
info = Introspector.getBeanInfo(baseObjectClass);
} else {
info = Introspector.getBeanInfo(baseObjectClass, Object.class);
}
Set<Method> methodsCovered = new HashSet<Method>();
PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
for (PropertyDescriptor desc : descriptors) {
if (desc.getName().startsWith(start) && (!IGNORED_METHODS.contains(desc.getName()))) {
List<Object> list = found.get(desc.getName());
if (list == null) {
list = new ArrayList<Object>();
found.put(desc.getName(), list);
}
list.add(desc);
methodsCovered.add(desc.getReadMethod());
methodsCovered.add(desc.getWriteMethod());
}
}
MethodDescriptor[] methods = info.getMethodDescriptors();
for (MethodDescriptor desc : methods) {
if (desc.getName().startsWith(start) && !methodsCovered.contains(desc.getMethod())
&& !desc.getName().startsWith("_d") && !IGNORED_METHODS.contains(desc.getName())) {
Method m = desc.getMethod();
List<Object> list = found.get(desc.getName());
if (list == null) {
list = new ArrayList<Object>();
found.put(desc.getName(), list);
}
list.add(m);
}
}
return found;
}
private String getSignature(Object object, Method m) {
StringBuilder buf = new StringBuilder();
Type[] params = m.getGenericParameterTypes();
int i = 0;
m = metadataProvider.getUnproxiedMethod(m);
buf.append(metadataProvider.getTypeName(m.getGenericReturnType(), false));
buf.append(" ");
buf.append(m.getName());
buf.append("(");
boolean first = true;
for (Type type : params) {
if (!first) {
buf.append(", ");
} else {
first = false;
}
String name = metadataProvider.getTypeName(type, false);
String paramName = metadataProvider.getParameterName(m, i);
if (paramName != null) {
name += " " + paramName;
}
buf.append(name);
i++;
}
buf.append(")");
return buf.toString();
}
@Override
public void setScriptContext(ScriptContext context) {
this.context = context;
}
private static Object invoke(Object o, Method m) throws IllegalAccessException, InvocationTargetException {
boolean access = m.isAccessible();
m.setAccessible(true);
try {
return m.invoke(o);
} finally {
m.setAccessible(access);
}
}
private static boolean isMethod(Object object) {
return object != null && object instanceof Method || object instanceof Function;
}
}