blob: 13f8747009c60b9fbef562d777cb44766f925889 [file] [log] [blame]
/*
* 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.netbeans.modules.java.hints.declarative;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.modules.java.hints.declarative.Condition.MethodInvocation.ParameterKind;
import org.netbeans.modules.java.hints.declarative.conditionapi.Context;
import org.netbeans.modules.java.hints.declarative.conditionapi.DefaultRuleUtilities;
import org.netbeans.modules.java.hints.declarative.conditionapi.Matcher;
import org.netbeans.modules.java.hints.declarative.conditionapi.Variable;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
/**
*
* @author lahvac
*/
public class MethodInvocationContext {
/*not private for tests*/final List<Class<?>> ruleUtilities;
public MethodInvocationContext() {
ruleUtilities = new LinkedList<Class<?>>();
ruleUtilities.add(DefaultRuleUtilities.class);
}
public Method linkMethod(String methodName, Map<? extends String, ? extends ParameterKind> params) {
Collection<Class<?>> paramTypes = new LinkedList<Class<?>>();
for (Entry<? extends String, ? extends ParameterKind> e : params.entrySet()) {
switch ((ParameterKind) e.getValue()) {
case VARIABLE:
paramTypes.add(Variable.class);
break;
case STRING_LITERAL:
paramTypes.add(String.class);
break;
case ENUM_CONSTANT:
Enum<?> constant = loadEnumConstant(e.getKey());
paramTypes.add(constant.getDeclaringClass());
break;
}
}
Method varArgMethod = null;
for (Class<?> clazz : ruleUtilities) {
OUTER: for (Method m : clazz.getDeclaredMethods()) {
if (methodName.equals(m.getName())) {
Class<?>[] p = m.getParameterTypes();
int c = 0;
Iterator<Class<?>> it = paramTypes.iterator();
for ( ; it.hasNext() && c < p.length; ) {
Class<?> paramClass = it.next();
Class<?> declaredClass = p[c++];
if (declaredClass.equals(paramClass))
continue;
if ( m.isVarArgs()
&& declaredClass.isArray()
&& declaredClass.getComponentType().equals(paramClass)
&& c == p.length) {
while (it.hasNext()) {
if (!paramClass.equals(it.next())) {
continue OUTER;
}
}
break;
}
continue OUTER;
}
if (!it.hasNext() && c == p.length) {
if (!m.isVarArgs()) {
return m;
}
if (varArgMethod == null) {
varArgMethod = m;
}
}
}
}
}
return varArgMethod;
}
public boolean invokeMethod(Context ctx, @NonNull Method method, Map<? extends String, ? extends ParameterKind> params) {
Collection<Object> paramValues = new LinkedList<Object>();
int i = 0;
Collection<Object> vararg = null;
for (Entry<? extends String, ? extends ParameterKind> e : params.entrySet()) {
if (++i == method.getParameterTypes().length && method.isVarArgs()) {
vararg = new LinkedList<Object>();
}
Object toAdd;
switch ((ParameterKind) e.getValue()) {
case VARIABLE:
toAdd = new Variable(e.getKey()); //TODO: security/safety
break;
case STRING_LITERAL:
toAdd = e.getKey();
break;
case ENUM_CONSTANT:
Enum<?> constant = loadEnumConstant(e.getKey());
toAdd = constant;
break;
default:
throw new IllegalStateException();
}
(vararg != null ? vararg : paramValues).add(toAdd);
}
if (method.isVarArgs()) {
Object[] arr = (Object[]) Array.newInstance(method.getParameterTypes()[method.getParameterTypes().length - 1].getComponentType(), vararg.size());
vararg.toArray(arr);
paramValues.add(arr);
}
Matcher matcher = new Matcher(ctx);
Class<?> clazz = method.getDeclaringClass();
try {
Constructor<?> c = clazz.getDeclaredConstructor(Context.class, Matcher.class);
method.setAccessible(true);
c.setAccessible(true);
Object instance = c.newInstance(ctx, matcher);
return (Boolean) method.invoke(instance, paramValues.toArray(new Object[0]));
} catch (InstantiationException ex) {
throw new IllegalStateException(ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException(ex);
} catch (IllegalAccessException ex) {
throw new IllegalStateException(ex);
} catch (IllegalArgumentException ex) {
throw new IllegalStateException(ex);
} catch (NoSuchMethodException ex) {
throw new IllegalStateException(ex);
} catch (SecurityException ex) {
throw new IllegalStateException(ex);
}
}
private static Enum<?> loadEnumConstant(String fqn) {
int lastDot = fqn.lastIndexOf('.');
assert lastDot != (-1);
String className = fqn.substring(0, lastDot);
String constantName = fqn.substring(lastDot + 1);
try {
Class c = (Class) Class.forName(className);
return Enum.valueOf(c, constantName);
} catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}
private static final AtomicInteger c = new AtomicInteger();
void setCode(String imports, Iterable<? extends String> blocks) {
if (!blocks.iterator().hasNext()) return ;
String className = "RuleUtilities$" + c.getAndIncrement();
StringBuilder code = new StringBuilder();
code.append("package $;\n");
for (String imp : AUXILIARY_IMPORTS) {
code.append(imp);
code.append("\n");
}
if (imports != null)
code.append(imports);
code.append("class " + className + "{\n");
code.append("private final Context context;\n");
code.append("private final Matcher matcher;\n");
code.append(className + "(Context context, Matcher matcher) {this.context = context; this.matcher = matcher;}");
for (String b : blocks) {
code.append(b);
}
code.append("}\n");
try {
final Map<String, byte[]> classes = Hacks.compile(computeCompileClassPath(), code.toString());
if (!classes.containsKey("$." + className)) {
//presumably an error in the custom code, skip
//TODO: should warn the if happens during an actual hint execution (as opposed to editting the hint in the editor)
return;
}
ClassLoader l = new ClassLoader(MethodInvocationContext.class.getClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (classes.containsKey(name)) {
byte[] bytes = classes.get(name);
return defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
};
Class<?> c = l.loadClass("$." + className);
ruleUtilities.add(c);
} catch (ClassNotFoundException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
static final String[] AUXILIARY_IMPORTS = new String[] {
"import org.netbeans.modules.java.hints.declarative.conditionapi.Context;",
"import org.netbeans.modules.java.hints.declarative.conditionapi.Matcher;",
"import org.netbeans.modules.java.hints.declarative.conditionapi.Variable;"
};
static ClassPath computeCompileClassPath() {
return ClassPathSupport.createClassPath(apiJarURL());
}
public static URL apiJarURL() {
URL jarFile = MethodInvocationContext.class.getProtectionDomain().getCodeSource().getLocation();
return FileUtil.urlForArchiveOrDir(FileUtil.archiveOrDirForURL(jarFile));
}
}