Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-39414] - Stapler 1.246 Binary Incompatibility. Part 2 #88

Merged
merged 7 commits into from
Nov 7, 2016
23 changes: 16 additions & 7 deletions core/src/main/java/org/kohsuke/stapler/lang/Klass.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;

/**
* Abstraction of class-like object, agnostic to languages.
Expand Down Expand Up @@ -56,14 +57,15 @@ public List<MethodRef> getDeclaredMethods() {
return navigator.getDeclaredMethods(clazz);
}

/**
* Gets list of fields declared by the class.
* @return List of fields.
* May return empty list in the case of obsolete {@link #navigator}, which does not offer the method.
* @since 1.246
*/
@Nonnull
public List<FieldRef> getDeclaredFields() {
try {
return navigator.getDeclaredFields(clazz);
} catch (AbstractMethodError err) {
// A plugin uses obsolete version of Stapler-dependent library (e.g. JRuby), which does not offer the method (JENKINS-39414)
// TODO: what to do with Logging? The error must be VERY visible, but it will totally pollute system logs
return Collections.emptyList();
}
return navigator.getDeclaredFields(clazz);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we need to test this change. You removed the try catch, but are sure that getDeclaredFields never throws an exception?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a hack in #85 in order to suppress the binary compatibility issue. Now we solve it by providing default implementations in KlassNavigator. Hence this AbstractMethodError handler is no longer required

}

/**
Expand All @@ -85,6 +87,13 @@ public List<FieldRef> getFields() {
return new ArrayList<FieldRef>(fields.values());
}

/**
* Reports all the methods that can be used for routing requests on this class.
* @return List of functions.
* May return empty list in the case of obsolete {@link #navigator}, which does not offer the method.
* @since 1.246
*/
@Nonnull
public List<Function> getFunctions() {
return navigator.getFunctions(clazz);
}
Expand Down
21 changes: 18 additions & 3 deletions core/src/main/java/org/kohsuke/stapler/lang/KlassNavigator.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import java.net.URL;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;

/**
* Strategy pattern to provide navigation across class-like objects in other languages of JVM.
Expand Down Expand Up @@ -78,15 +80,28 @@ public abstract class KlassNavigator<C> {

/**
* List fields of this class.
*
* This list excludes fields from super classes.
* @param clazz Class
* @return List of the fields declared for the class.
* By default this list is empty, {@link KlassNavigator} implementations are responsible to implement it.
* @since 1.246
*/
public abstract List<FieldRef> getDeclaredFields(C clazz);
@Nonnull
public List<FieldRef> getDeclaredFields(C clazz) {
return Collections.emptyList();
}

/**
* Reports all the methods that can be used for routing requests on this class.
* @param clazz Class
* @return List of the fields functions declared for the class.
* By default this list is empty, {@link KlassNavigator} implementations are responsible to implement it.
* @since 1.246
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW getDeclaredMethods was also added as abstract somewhat recently (1.220), but I suppose if this caused problems we would have hit them long since.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I decided not to touch that code. Ruby Runtime has a dep on a greater version

*/
public abstract List<Function> getFunctions(C clazz);
@Nonnull
public List<Function> getFunctions(C clazz) {
return Collections.emptyList();
}

/**
* If the given type is an array that supports index retrieval.
Expand Down
20 changes: 19 additions & 1 deletion core/src/main/java/org/kohsuke/stapler/lang/MethodRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.annotation.CheckForNull;

/**
* @author Kohsuke Kawaguchi
Expand All @@ -16,6 +17,18 @@ public abstract class MethodRef extends AnnotatedRef {
public boolean isRoutable() {
return true;
}

/**
* Retrieves the referenced method name.
* Some implementations (e.g. Ruby) cannot guarantee availability of names for all cases,
* so sometimes the name may be missing.
* @return Method name. {@code null} if it cannot be determined.
* @since 1.248
*/
@CheckForNull
public String getName() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a proposed caller for this, or is it just KlassTest?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this API is generally useful, e.g. for diagnostic purposes. Right now just in KlassTest and RubyKlassNavigatorTest. The latter one is supposed to be an external lib.

return null;
}

public abstract Object invoke(Object _this, Object... args) throws InvocationTargetException, IllegalAccessException;

Expand All @@ -33,7 +46,12 @@ public boolean isRoutable() {
if (m.isBridge()) return false;
return (m.getModifiers() & Modifier.PUBLIC)!=0;
}


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

@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 @@ -19,6 +19,11 @@ public boolean isRoutable() {
return getBase().isRoutable();
}

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

@Override
public Object invoke(Object _this, Object... args) throws InvocationTargetException, IllegalAccessException {
return getBase().invoke(_this, args);
Expand Down
32 changes: 32 additions & 0 deletions core/src/test/java/org/kohsuke/stapler/lang/KlassTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@
import org.junit.Test;

import java.util.List;
import org.kohsuke.stapler.Function;

/**
* Contains tests for {@link Klass}.
* @author Oleg Nenashev.
*/
public class KlassTest {

@Test
public void shouldProperlyAccessJavaDeclaredMethods() throws Exception {
final Klass<Class> classInstance = Klass.java(FooClass.class);
final List<MethodRef> declaredFunctions = classInstance.getDeclaredMethods();
for (MethodRef ref : declaredFunctions) {
if ("doDynamic".equals(ref.getName())) {
//TODO: check field parameters once Stapler provides such info
return;
}
}
Assert.fail("Have not found the 'doDynamic' declared method for FooClass");
}

@Test
public void shouldProperlyAccessJavaDeclaredFields() throws Exception {
final Klass<Class> classInstance = Klass.java(FooClass.class);
Expand All @@ -23,8 +37,26 @@ public void shouldProperlyAccessJavaDeclaredFields() throws Exception {
}
Assert.fail("Have not found 'fooField' in the returned field list");
}

@Test
public void shouldProperlyAccessJavaDeclaredFunctions() throws Exception {
final Klass<Class> classInstance = Klass.java(FooClass.class);
final List<Function> declaredFunctions = classInstance.getFunctions();
for (Function ref : declaredFunctions) {
if ("doDynamic".equals(ref.getName())) {
//TODO: check field parameters once Stapler provides such info
return;
}
}
Assert.fail("Have not found 'doDynamic' function for FooClass");
}

private static final class FooClass {
private int fooField;

public Object doDynamic(String token) {
// Just return something potentially routable
return new Integer(0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,48 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import javax.annotation.Nonnull;

/**
* @author Kohsuke Kawaguchi
*/
public class RubyMethodRef extends MethodRef {
@Nonnull
private final RubyModule klass;
@Nonnull
private final DynamicMethod method;


public RubyMethodRef(RubyModule klass, DynamicMethod method) {
public RubyMethodRef(@Nonnull RubyModule klass, @Nonnull DynamicMethod method) {
this.klass = klass;
this.method = method;
}

/**
* Retrieves the Ruby module (aka class), for which the method is declared.
* @return Ruby module, which stores the method reference
* @since 1.248
*/
@Nonnull
public RubyModule getKlass() {
return klass;
}

/**
* Retrieves the referenced method.
* @return Referenced method
* @since 1.248
*/
@Nonnull
public DynamicMethod getMethod() {
return method;
}

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

@Override
public <T extends Annotation> T getAnnotation(Class<T> type) {
// TODO: what's the equivalent in JRuby?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,59 @@

import static org.hamcrest.CoreMatchers.*;
import org.hamcrest.collection.IsEmptyCollection;
import org.jruby.RubyFixnum;
import org.jruby.RubyInteger;
import org.jruby.internal.runtime.methods.CallConfiguration;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import static org.junit.Assume.assumeThat;
import org.kohsuke.stapler.Function;
import org.kohsuke.stapler.lang.MethodRef;

/**
* Tests handling of Ruby classes and modules with {@link RubyKlassNavigator}.
* @author Oleg Nenashev.
*/
public class RubyKlassNavigatorTest {

/**
* Verifies that declared method retrieval works well.
* Effective use-case - Ruby Runtime Plugin for Jenkins.
* @throws Exception Test failure
*/
@Test
public void shouldProperlyHandleDeclaredMethodsInRubyModules() throws Exception {
final ScriptingContainer ruby = createRubyInstance();
final RubyKlassNavigator navigator = new RubyKlassNavigator(ruby.getProvider().getRuntime(), ClassLoader.getSystemClassLoader());
final MyRubyModule myModule = new MyRubyModule(ruby.getRuntime(), new MyRubyClass(ruby.getRuntime()), true);


final Klass<RubyModule> classInstance = new Klass<RubyModule>(myModule, navigator);

final List<MethodRef> declaredMethods = classInstance.getDeclaredMethods();
for (MethodRef ref : declaredMethods) {
if (ref instanceof RubyMethodRef) {
// Ruby engine API allows creating methods with null names, not our bug BTW...
if ("doDynamic".equals(ref.getName())) {
//TODO: More consistency checks
return;
}
}
}
Assert.fail("Have not found 'doDynamic' in the returned function list");
}

/**
* Verifies that field retrieval do not fail horribly for {@link RubyModule}.
* Effective use-case - Ruby Runtime Plugin for Jenkins.
* @throws Exception Test failure
*/
@Test
@Issue("JENKINS-39414")
public void shouldProperlyHandleRubyModules() throws Exception {
public void shouldProperlyHandleFieldsInRubyModules() throws Exception {
final ScriptingContainer ruby = createRubyInstance();
final RubyKlassNavigator navigator = new RubyKlassNavigator(ruby.getProvider().getRuntime(), ClassLoader.getSystemClassLoader());
final MyRubyModule myModule = new MyRubyModule(ruby.getRuntime(), new MyRubyClass(ruby.getRuntime()), true);
Expand All @@ -51,19 +88,66 @@ public void shouldProperlyHandleRubyModules() throws Exception {
Assert.fail("Have not found 'fooField' in the returned field list");
}

private static final class MyRubyModule extends RubyModule {
//TODO: fix the test when Ruby routing gets implemented (https://github.com/stapler/stapler/issues/87)
/**
* Verifies that function retrieval do not fail horribly for {@link RubyModule}.
* Effective use-case - Ruby Runtime Plugin for Jenkins.
* @throws Exception Test failure
*/
@Test
@Issue("JENKINS-39414")
public void shouldProperlyHandleFunctionsInRubyModules() throws Exception {
final ScriptingContainer ruby = createRubyInstance();
final RubyKlassNavigator navigator = new RubyKlassNavigator(ruby.getProvider().getRuntime(), ClassLoader.getSystemClassLoader());
final MyRubyModule myModule = new MyRubyModule(ruby.getRuntime(), new MyRubyClass(ruby.getRuntime()), true);

private int fooField = 123;

final Klass<RubyModule> classInstance = new Klass<RubyModule>(myModule, navigator);

final List<Function> declaredFunctions = classInstance.getFunctions();
for (Function ref : declaredFunctions) {
if ("doDynamic".equals(ref.getName())) {
//TODO: check fields once implemented in Stapler
return;
}
}

assumeThat("Routing of declared routable methods is not fully implemented (See https://github.com/stapler/stapler/issues/87)",
ruby, not(anything("Nothing to do in this code")));
Assert.fail("Have not found 'doDynamic' in the returned function list");
}

private static final class MyRubyModule extends RubyModule {

private int fooField = 123;

MyRubyModule(Ruby runtime, RubyClass metaClass, boolean objectSpace) {
super(runtime, metaClass, objectSpace);

// Register a Mock method for the class
getMethodsForWrite().put("doDynamic", new DynamicMethod(this, Visibility.PUBLIC, CallConfiguration.FrameFullScopeFull, "doDynamic") {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
return doDynamic("foo");
}

@Override
public DynamicMethod dup() {
throw new UnsupportedOperationException("Not supported yet.");
}
});
}

public final IRubyObject doDynamic(String token) {
// Just return a routable object
return new RubyFixnum(getRuntime(), 0);
}
}

private static final class MyRubyClass extends RubyClass {
private static final class MyRubyClass extends RubyClass {
MyRubyClass(Ruby runtime) {
super(runtime);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gratuitous whitespace changes.

}

private ScriptingContainer createRubyInstance() {
Expand Down