
org.kohsuke.stapler.MetaClass Maven / Gradle / Ivy
Show all versions of stapler-core Show documentation
/*
* Copyright (c) 2004-2010, Kohsuke Kawaguchi
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.kohsuke.stapler;
import net.sf.json.JSONArray;
import org.apache.commons.io.IOUtils;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import javax.servlet.ServletException;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* The stapler version of the {@link Class} object,
* that retains some useful cache about a class and its view.
*
* @author Kohsuke Kawaguchi
* @see WebApp#getMetaClass(Class)
*/
public class MetaClass extends TearOffSupport {
/**
* This meta class wraps this class
*/
public final Class clazz;
/**
* {@link MetaClassLoader} that wraps {@code clazz.getClassLoader()}.
* Null if the class is loaded by the bootstrap classloader.
*/
public final MetaClassLoader classLoader;
public final List dispatchers = new ArrayList();
/**
* Base metaclass.
* Note that baseClass.clazz==clazz.getSuperClass()
*/
public final MetaClass baseClass;
/**
* {@link WebApp} that owns this meta class.
*/
public final WebApp webApp;
/*package*/ MetaClass(WebApp webApp, Class clazz) {
this.clazz = clazz;
this.webApp = webApp;
this.baseClass = webApp.getMetaClass(clazz.getSuperclass());
this.classLoader = MetaClassLoader.get(clazz.getClassLoader());
buildDispatchers(
new ClassDescriptor(clazz,null/*support wrappers*/));
}
/**
* Build {@link #dispatchers}.
*
*
* This is the meat of URL dispatching. It looks at the class
* via reflection and figures out what URLs are handled by who.
*/
private void buildDispatchers( ClassDescriptor node ) {
// check action .do(...)
for( final Function f : node.methods.prefix("do") ) {
WebMethod a = f.getAnnotation(WebMethod.class);
String[] names;
if(a!=null && a.name().length>0) names=a.name();
else names=new String[]{camelize(f.getName().substring(2))}; // 'doFoo' -> 'foo'
for (String name : names) {
dispatchers.add(new NameBasedDispatcher(name,0) {
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
if(traceable())
trace(req,rsp,"-> <%s>.%s(...)",node,f.getName());
f.bindAndInvokeAndServeResponse(node, req, rsp);
return true;
}
public String toString() {
return f.getQualifiedName()+"(...) for url=/"+name+"/...";
}
});
}
}
// JavaScript proxy method invocations for js
// reacts only to a specific content type
for( final Function f : node.methods.prefix("js") ) {
String name = camelize(f.getName().substring(2)); // jsXyz -> xyz
dispatchers.add(new JavaScriptProxyMethodDispatcher(name, f));
}
// JavaScript proxy method with @JavaScriptMethod
// reacts only to a specific content type
for( final Function f : node.methods.annotated(JavaScriptMethod.class) ) {
JavaScriptMethod a = f.getAnnotation(JavaScriptMethod.class);
String[] names;
if(a!=null && a.name().length>0) names=a.name();
else names=new String[]{f.getName()};
for (String name : names)
dispatchers.add(new JavaScriptProxyMethodDispatcher(name,f));
}
for (Facet f : webApp.facets)
f.buildViewDispatchers(this, dispatchers);
// check action .doIndex(...)
for( final Function f : node.methods.name("doIndex") ) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
if(req.tokens.hasMore())
return false; // applicable only when there's no more token
if(traceable())
trace(req,rsp,"-> <%s>.doIndex(...)",node);
f.bindAndInvokeAndServeResponse(node,req,rsp);
return true;
}
public String toString() {
return f.getQualifiedName()+"(StaplerRequest,StaplerResponse) for url=/";
}
});
}
// check public properties of the form NODE.TOKEN
for (final Field f : node.fields) {
dispatchers.add(new NameBasedDispatcher(f.getName()) {
final String role = getProtectedRole(f);
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException {
if(role!=null && !req.isUserInRole(role))
throw new IllegalAccessException("Needs to be in role "+role);
if(traceable())
traceEval(req,rsp,node,f.getName());
req.getStapler().invoke(req, rsp, f.get(node));
return true;
}
public String toString() {
return String.format("%1$s.%2$s for url=/%2$s/...",f.getDeclaringClass().getName(),f.getName());
}
});
}
FunctionList getMethods = node.methods.prefix("get");
// check public selector methods of the form NODE.getTOKEN()
for( final Function f : getMethods.signature() ) {
if(f.getName().length()<=3)
continue;
WebMethod a = f.getAnnotation(WebMethod.class);
String[] names;
if(a!=null && a.name().length>0) names=a.name();
else names=new String[]{camelize(f.getName().substring(3))}; // 'getFoo' -> 'foo'
for (String name : names) {
dispatchers.add(new NameBasedDispatcher(name) {
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
if(traceable())
traceEval(req,rsp,node,f.getName()+"()");
req.getStapler().invoke(req,rsp, f.invoke(req, node));
return true;
}
public String toString() {
return String.format("%1$s() for url=/%2$s/...",f.getQualifiedName(),name);
}
});
}
}
// check public selector methods of the form static NODE.getTOKEN(StaplerRequest)
for( final Function f : getMethods.signature(StaplerRequest.class) ) {
if(f.getName().length()<=3)
continue;
String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
dispatchers.add(new NameBasedDispatcher(name) {
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
if(traceable())
traceEval(req,rsp,node,f.getName()+"(...)");
req.getStapler().invoke(req,rsp, f.invoke(req, node, req));
return true;
}
public String toString() {
return String.format("%1$s(StaplerRequest) for url=/%2$s/...",f.getQualifiedName(),name);
}
});
}
// check public selector methods .get(String)
for( final Function f : getMethods.signature(String.class) ) {
if(f.getName().length()<=3)
continue;
String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
dispatchers.add(new NameBasedDispatcher(name,1) {
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
String token = req.tokens.next();
if(traceable())
traceEval(req,rsp,node,f.getName()+"(\""+token+"\")");
req.getStapler().invoke(req,rsp, f.invoke(req,node,token));
return true;
}
public String toString() {
return String.format("%1$s(String) for url=/%2$s/TOKEN/...",f.getQualifiedName(),name);
}
});
}
// check public selector methods .get(int)
for( final Function f : getMethods.signature(int.class) ) {
if(f.getName().length()<=3)
continue;
String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
dispatchers.add(new NameBasedDispatcher(name,1) {
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
int idx = req.tokens.nextAsInt();
if(traceable())
traceEval(req,rsp,node,f.getName()+"("+idx+")");
req.getStapler().invoke(req,rsp, f.invoke(req,node,idx));
return true;
}
public String toString() {
return String.format("%1$s(int) for url=/%2$s/N/...",f.getQualifiedName(),name);
}
});
}
if(node.clazz.isArray()) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
return false;
try {
int index = req.tokens.nextAsInt();
if(traceable())
traceEval(req,rsp,node,"((Object[])",")["+index+"]");
req.getStapler().invoke(req,rsp, ((Object[]) node)[index]);
return true;
} catch (NumberFormatException e) {
return false; // try next
}
}
public String toString() {
return "Array look-up for url=/N/...";
}
});
}
if(List.class.isAssignableFrom(node.clazz)) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
return false;
try {
int index = req.tokens.nextAsInt();
if(traceable())
traceEval(req,rsp,node,"((List)",").get("+index+")");
List list = (List) node;
if (0<=index && index IndexOutOfRange [0,%d)",list.size());
rsp.sendError(SC_NOT_FOUND);
}
return true;
} catch (NumberFormatException e) {
return false; // try next
}
}
public String toString() {
return "List.get(int) look-up for url=/N/...";
}
});
}
if(Map.class.isAssignableFrom(node.clazz)) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
return false;
try {
String key = req.tokens.peek();
if(traceable())
traceEval(req,rsp,"((Map)",").get(\""+key+"\")");
Object item = ((Map)node).get(key);
if(item!=null) {
req.tokens.next();
req.getStapler().invoke(req,rsp,item);
return true;
} else {
// otherwise just fall through
if(traceable())
trace(req,rsp,"Map.get(\""+key+"\")==null. Back tracking.");
return false;
}
} catch (NumberFormatException e) {
return false; // try next
}
}
public String toString() {
return "Map.get(String) look-up for url=/TOKEN/...";
}
});
}
// TODO: check if we can route to static resources
// which directory shall we look up a resource from?
// check action .doDynamic(...)
for( final Function f : node.methods.name("doDynamic") ) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
if(traceable())
trace(req,rsp,"-> <%s>.doDynamic(...)",node);
f.bindAndInvokeAndServeResponse(node,req,rsp);
return true;
}
public String toString() {
return String.format("%s(StaplerRequest,StaplerResponse) for any URL",f.getQualifiedName());
}
});
}
// check public selector methods .getDynamic(,...)
for( final Function f : getMethods.signatureStartsWith(String.class).name("getDynamic")) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, IOException, ServletException {
if(!req.tokens.hasMore())
return false;
String token = req.tokens.next();
if(traceable())
traceEval(req,rsp,node,"getDynamic(\""+token+"\",...)");
Object target = f.bindAndInvoke(node, req,rsp, token);
if(target!=null) {
req.getStapler().invoke(req,rsp, target);
return true;
} else {
if(traceable())
// indent: "-> evaluate(
trace(req,rsp," %s.getDynamic(\"%s\",...)==null. Back tracking.",node,token);
req.tokens.prev(); // cancel the next effect
return false;
}
}
public String toString() {
return String.format("%s(String,StaplerRequest,StaplerResponse) for url=/TOKEN/...",f.getQualifiedName());
}
});
}
}
private String getProtectedRole(Field f) {
try {
LimitedTo a = f.getAnnotation(LimitedTo.class);
return (a!=null)?a.value():null;
} catch (LinkageError e) {
return null; // running in JDK 1.4
}
}
private static String camelize(String name) {
return Character.toLowerCase(name.charAt(0))+name.substring(1);
}
private static class JavaScriptProxyMethodDispatcher extends NameBasedDispatcher {
private final Function f;
public JavaScriptProxyMethodDispatcher(String name, Function f) {
super(name, 0);
this.f = f;
}
public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
if (!req.isJavaScriptProxyCall())
return false;
req.stapler.getWebApp().getCrumbIssuer().validateCrumb(req,req.getHeader("Crumb"));
if(traceable())
trace(req,rsp,"-> <%s>.%s(...)",node, f.getName());
JSONArray jsargs = JSONArray.fromObject(IOUtils.toString(req.getReader()));
Object[] args = new Object[jsargs.size()];
Class[] types = f.getParameterTypes();
Type[] genericTypes = f.getParameterTypes();
for (int i=0; i