Skip to content

Commit

Permalink
Generalize multi-lingual support of Stapler to include routing
Browse files Browse the repository at this point in the history
Originally this capability was added to support JRuby, and so far it was
only used to find @PostConstruct method.

But if we expand this Klass and KlassNavigator abstraction to request
routing, then we can use this to build a parallel type hierarchy.

See issue #76
  • Loading branch information
kohsuke committed Jun 15, 2016
1 parent e61507e commit 4f12b1e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 13 deletions.
29 changes: 29 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/KlassDescriptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.kohsuke.stapler;

import org.kohsuke.stapler.lang.FieldRef;
import org.kohsuke.stapler.lang.Klass;

import java.util.List;

/**
* Reflection information of a {@link Klass} that drives the request routing.
*
* <p>
* Born as a generalization of {@link ClassDescriptor} to {@link Klass}.
* @author Kohsuke Kawaguchi
*/
class KlassDescriptor<C> {
final Klass<C> clazz;
final FunctionList methods;
final List<FieldRef> fields;

/**
* @param klazz
* The class to build a descriptor around.
*/
public KlassDescriptor(Klass<C> klazz) {
this.clazz = klazz;
this.fields = klazz.getFields();
this.methods = new FunctionList(klazz.getFunctions());
}
}
19 changes: 11 additions & 8 deletions core/src/main/java/org/kohsuke/stapler/MetaClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
import net.sf.json.JSONArray;
import org.apache.commons.io.IOUtils;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import org.kohsuke.stapler.lang.FieldRef;
import org.kohsuke.stapler.lang.Klass;
import org.kohsuke.stapler.lang.MethodRef;

import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
Expand Down Expand Up @@ -103,7 +103,7 @@ public class MetaClass extends TearOffSupport {
*/
/*package*/ void buildDispatchers() {
this.dispatchers.clear();
ClassDescriptor node = new ClassDescriptor(clazz,null/*TODO:support wrappers*/);
KlassDescriptor<?> node = new KlassDescriptor(klass);

// check action <obj>.do<token>(...) and other WebMethods
for( final Function f : node.methods.webMethods() ) {
Expand Down Expand Up @@ -162,7 +162,7 @@ public String toString() {
}

// check public properties of the form NODE.TOKEN
for (final Field f : node.fields) {
for (final FieldRef 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 {
Expand All @@ -175,7 +175,7 @@ public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws
return true;
}
public String toString() {
return String.format("%1$s.%2$s for url=/%2$s/...",f.getDeclaringClass().getName(),f.getName());
return String.format("%1$s for url=/%2$s/...",f.getQualifiedName(),f.getName());
}
});
}
Expand Down Expand Up @@ -278,7 +278,8 @@ public String toString() {
});
}

if(node.clazz.isArray()) {
// TODO: Klass needs to be able to define its array like access
if(node.clazz.toJavaClass().isArray()) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
Expand All @@ -299,7 +300,8 @@ public String toString() {
});
}

if(List.class.isAssignableFrom(node.clazz)) {
// TODO: Klass needs to be able to define its list like access
if(List.class.isAssignableFrom(node.clazz.toJavaClass())) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
Expand Down Expand Up @@ -328,7 +330,8 @@ public String toString() {
});
}

if(Map.class.isAssignableFrom(node.clazz)) {
// TODO: Klass needs to be able to define its map like access
if(Map.class.isAssignableFrom(node.clazz.toJavaClass())) {
dispatchers.add(new Dispatcher() {
public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
if(!req.tokens.hasMore())
Expand Down Expand Up @@ -430,7 +433,7 @@ public SingleLinkedList<MethodRef> getPostConstructMethods() {
return postConstructMethods;
}

private String getProtectedRole(Field f) {
private String getProtectedRole(FieldRef f) {
try {
LimitedTo a = f.getAnnotation(LimitedTo.class);
return (a!=null)?a.value():null;
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/lang/AnnotatedRef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.kohsuke.stapler.lang;

import java.lang.annotation.Annotation;

/**
* @author Kohsuke Kawaguchi
*/
public abstract class AnnotatedRef {
// no subtyping outside the package
/*package*/ AnnotatedRef() {}

public abstract <T extends Annotation> T getAnnotation(Class<T> type);

public boolean hasAnnotation(Class<? extends Annotation> type) {
return getAnnotation(type)!=null;
}
}
52 changes: 52 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/lang/FieldRef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.kohsuke.stapler.lang;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

/**
* @author Kohsuke Kawaguchi
*/
public abstract class FieldRef extends AnnotatedRef {
/**
* Name of the method.
*
* @see Field#getName()
*/
public abstract String getName();

/**
* Obtains the value of the field of the instance.
*/
public abstract Object get(Object instance) throws IllegalAccessException;

/**
* Gets a fully qualified name of this field that includes the declaring type.
*/
public abstract String getQualifiedName();

public static FieldRef wrap(final Field f) {
f.setAccessible(true);

return new FieldRef() {
@Override
public <T extends Annotation> T getAnnotation(Class<T> type) {
return f.getAnnotation(type);
}

@Override
public String getName() {
return f.getName();
}

@Override
public Object get(Object instance) throws IllegalAccessException {
return f.get(instance);
}

@Override
public String getQualifiedName() {
return f.getDeclaringClass().getName()+"."+getName();
}
};
}
}
10 changes: 10 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/lang/Klass.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.kohsuke.stapler.lang;

import org.kohsuke.stapler.Function;

import java.net.URL;
import java.util.List;

Expand Down Expand Up @@ -50,6 +52,14 @@ public List<MethodRef> getDeclaredMethods() {
return navigator.getDeclaredMethods(clazz);
}

public List<FieldRef> getFields() {
return navigator.getDeclaredFields(clazz);
}

public List<Function> getFunctions() {
return navigator.getFunctions(clazz);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
23 changes: 23 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/lang/KlassNavigator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.kohsuke.stapler.lang;

import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.Function;
import org.kohsuke.stapler.MetaClassLoader;

import java.lang.reflect.Method;
Expand Down Expand Up @@ -71,6 +73,15 @@ public abstract class KlassNavigator<C> {
*/
public abstract List<MethodRef> getDeclaredMethods(C clazz);

/**
* List fields of this class.
*
* This list excludes fields from super classes.
*/
public abstract List<FieldRef> getDeclaredFields(C clazz);

public abstract List<Function> getFunctions(C clazz);

public static final KlassNavigator<Class> JAVA = new KlassNavigator<Class>() {
@Override
public URL getResource(Class clazz, String resourceName) {
Expand Down Expand Up @@ -125,5 +136,17 @@ public int size() {
}
};
}

@Override
public List<FieldRef> getDeclaredFields(Class clazz) {
return null;
}

@Override
public List<Function> getFunctions(Class clazz) {
// Historically ClassDescriptor used to own this non-trivial logic of computing
// valid functions for the class, so we'll keep it there.
return new ClassDescriptor(clazz).methods;
}
};
}
18 changes: 13 additions & 5 deletions core/src/main/java/org/kohsuke/stapler/lang/MethodRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
* @author Kohsuke Kawaguchi
* @since 1.220
*/
public abstract class MethodRef {
public abstract <T extends Annotation> T getAnnotation(Class<T> type);

public boolean hasAnnotation(Class<? extends Annotation> type) {
return getAnnotation(type)!=null;
public abstract class MethodRef extends AnnotatedRef {
/**
* Returns true if this method is a 'public' method that should be used for routing requests.
*/
public boolean isRoutable() {
return true;
}

public abstract Object invoke(Object _this, Object... args) throws InvocationTargetException, IllegalAccessException;
Expand All @@ -26,6 +28,12 @@ public <T extends Annotation> T getAnnotation(Class<T> type) {
return m.getAnnotation(type);
}

@Override
public boolean isRoutable() {
if (m.isBridge()) return false;
return (m.getModifiers() & Modifier.PUBLIC)!=0;
}

@Override
public Object invoke(Object _this, Object... args) throws InvocationTargetException, IllegalAccessException {
return m.invoke(_this,args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.Function;
import org.kohsuke.stapler.MetaClassLoader;
import org.kohsuke.stapler.lang.FieldRef;
import org.kohsuke.stapler.lang.Klass;
import org.kohsuke.stapler.lang.KlassNavigator;
import org.kohsuke.stapler.lang.MethodRef;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/**
* {@link KlassNavigator} implementation for JRuby.
*
* @author Kohsuke Kawaguchi
*/
public class RubyKlassNavigator extends KlassNavigator<RubyModule> {
Expand Down Expand Up @@ -72,6 +78,18 @@ public List<MethodRef> getDeclaredMethods(RubyModule clazz) {
return r;
}

@Override
public List<FieldRef> getDeclaredFields(RubyModule clazz) {
// IIUC, Ruby doesn't have statically defined instance fields
return Collections.emptyList();
}

@Override
public List<Function> getFunctions(RubyModule clazz) {
// implemented as a fallback to Java through reified class, but maybe there's a better way to do this
return new ClassDescriptor(toJavaClass(clazz)).methods;
}

@Override
public Class toJavaClass(RubyModule clazz) {
if (clazz instanceof RubyClass) {
Expand Down

0 comments on commit 4f12b1e

Please sign in to comment.