Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-27927

Some interface idioms do not work in Groovy CPS

      Since class inheritance is not available to groovy workflow scripts, I thought I'd try some alternatives, so I gave "interfaces" a go instead. I want to specify an interface that provides the templating for a common behavior I want to achieve.

      e.g. MyFoo.groovy could implement an interface specified in Foo.groovy. Other scripts e.g. Bar.groovy could then invoke a common interface specified in YourFoo.groovy, MyFoo.groovy, RonaldsFoo.groovy and so on.

      In trying out a very simple example of an interface to achieve what I want, the groovy-cps compiler errors.

      Here is the valid groovy code that I tried (and works) under a groovy console:

      interface Foo {
      String name()
      }
      
      
      b = [name: {return "Ronald"}]
      a = b as Foo
      
      println a.name()
      
      return
      

      This prints "Ronald" as you'd expect.

      The code that I tried in an example workflow, I was hoping could do the same, but does not:

      node {
          b = [name: {return "Ronald"}]
          a = b as Foo
      
          echo a.name()
      }
      
      interface Foo {
      String name()
      }
      

      The error that is thrown when I hit "Apply" after entering the code in the Groovy CPS DSL box is:

      javax.servlet.ServletException: java.lang.NullPointerException: Cannot invoke method visit() on null object
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:796)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.MetaClass$6.doDispatch(MetaClass.java:249)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.MetaClass$6.doDispatch(MetaClass.java:249)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
      	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:649)
      	at org.kohsuke.stapler.Stapler.service(Stapler.java:238)
      	at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
      	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:686)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1494)
      	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:123)
      	at hudson.plugins.greenballs.GreenBallFilter.doFilter(GreenBallFilter.java:58)
      	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:120)
      	at hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:114)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:48)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:84)
      	at hudson.security.ChainedServletFilter.doFilter(ChainedServletFilter.java:76)
      	at hudson.security.HudsonFilter.doFilter(HudsonFilter.java:168)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at org.kohsuke.stapler.compression.CompressionFilter.doFilter(CompressionFilter.java:49)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:81)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
      	at org.kohsuke.stapler.DiagnosticThreadNameFilter.doFilter(DiagnosticThreadNameFilter.java:30)
      	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1474)
      	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:499)
      	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137)
      	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:533)
      	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231)
      	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1086)
      	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:428)
      	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193)
      	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1020)
      	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135)
      	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116)
      	at org.eclipse.jetty.server.Server.handle(Server.java:370)
      	at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:489)
      	at org.eclipse.jetty.server.AbstractHttpConnection.content(AbstractHttpConnection.java:960)
      	at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.content(AbstractHttpConnection.java:1021)
      	at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:865)
      	at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:240)
      	at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82)
      	at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:668)
      	at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52)
      	at winstone.BoundedExecutorService$1.run(BoundedExecutorService.java:77)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
      	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
      	at java.lang.Thread.run(Thread.java:619)
      Caused by: java.lang.NullPointerException: Cannot invoke method visit() on null object
      	at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:77)
      	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:45)
      	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
      	at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:32)
      	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
      	at org.codehaus.groovy.ast.stmt.BlockStatement$visit$0.call(Unknown Source)
      	at com.cloudbees.groovy.cps.CpsTransformer.visitMethod(CpsTransformer.groovy:159)
      	at sun.reflect.GeneratedMethodAccessor104.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
      	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
      	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:361)
      	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:903)
      	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:66)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:141)
      	at com.cloudbees.groovy.cps.CpsTransformer$_call_closure1.doCall(CpsTransformer.groovy:88)
      	at sun.reflect.GeneratedMethodAccessor1139.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
      	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
      	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:272)
      	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:903)
      	at groovy.lang.Closure.call(Closure.java:415)
      	at groovy.lang.Closure.call(Closure.java:428)
      	at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:1379)
      	at org.codehaus.groovy.runtime.DefaultGroovyMethods.each(DefaultGroovyMethods.java:1351)
      	at org.codehaus.groovy.runtime.dgm$170.invoke(Unknown Source)
      	at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite$PojoMetaMethodSiteNoUnwrapNoCoerce.invoke(PojoMetaMethodSite.java:271)
      	at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:53)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callSafe(AbstractCallSite.java:82)
      	at com.cloudbees.groovy.cps.CpsTransformer.call(CpsTransformer.groovy:88)
      	at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:970)
      	at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:548)
      	at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:526)
      	at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:503)
      	at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:302)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:281)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:267)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:214)
      	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:224)
      	at org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition$DescriptorImpl.doCheckScript(CpsFlowDefinition.java:123)
      	at sun.reflect.GeneratedMethodAccessor451.invoke(Unknown Source)
      	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      	at java.lang.reflect.Method.invoke(Method.java:597)
      	at org.kohsuke.stapler.Function$InstanceFunction.invoke(Function.java:298)
      	at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:161)
      	at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:96)
      	at org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:121)
      	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
      	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
      	... 54 more
      

      So although this support for groovy interfaces does not appear to be supported - is it worthy of consideration?

          [JENKINS-27927] Some interface idioms do not work in Groovy CPS

          Thomas Dalton created issue -

          Jesse Glick added a comment -

          Yet another exotic Groovy syntax which is apparently not yet supported by groovy-cps. I would suggest sticking to more conservative Java-like idioms which are more likely to work.

          Jesse Glick added a comment - Yet another exotic Groovy syntax which is apparently not yet supported by groovy-cps . I would suggest sticking to more conservative Java-like idioms which are more likely to work.
          Jesse Glick made changes -
          Assignee Original: Jesse Glick [ jglick ] New: Kohsuke Kawaguchi [ kohsuke ]
          Issue Type Original: New Feature [ 2 ] New: Bug [ 1 ]
          Summary Original: Use of "interface" in workflow scripts wont compile New: Some interface idioms do not work in Groovy CPS

          Thomas Dalton added a comment -

          Hi Jesse,

          Thanks for getting back to me again.

          The reason I'm looking at the workflow-plugin is because it allows a programmatic interface, and it is potentially a very powerful means to share jenkins job setups across multiple teams and engineers, and even more so if it can work in a way that provides the extensibility and reuse that the groovy language can offer. This means things like class inheritance, interfaces and other OO techniques are desirable and of particular interest to me. Moreover, the flows I'm looking at are inherently quite complex and I would ideally like to generate infra for them that could be used by a number of people/teams - not just copy/pasted.

          If I go with conservative, I fear I will end up with multiple copies of the same rehashed code for each instance where it is actually used - and that means more places for it to go wrong and maintain.

          So what would really set the workflow-plugin apart from e.g. a typical multijob setup is the ability to do some of the less "conservative", more "groovy" things

          Regards,
          Tom.

          Thomas Dalton added a comment - Hi Jesse, Thanks for getting back to me again. The reason I'm looking at the workflow-plugin is because it allows a programmatic interface, and it is potentially a very powerful means to share jenkins job setups across multiple teams and engineers, and even more so if it can work in a way that provides the extensibility and reuse that the groovy language can offer. This means things like class inheritance, interfaces and other OO techniques are desirable and of particular interest to me. Moreover, the flows I'm looking at are inherently quite complex and I would ideally like to generate infra for them that could be used by a number of people/teams - not just copy/pasted. If I go with conservative, I fear I will end up with multiple copies of the same rehashed code for each instance where it is actually used - and that means more places for it to go wrong and maintain. So what would really set the workflow-plugin apart from e.g. a typical multijob setup is the ability to do some of the less "conservative", more "groovy" things Regards, Tom.

          Jesse Glick added a comment -

          Sure, the intention is to support all Groovy language features, but there is a way to go before we get there.

          Jesse Glick added a comment - Sure, the intention is to support all Groovy language features, but there is a way to go before we get there.

          Thomas Dalton added a comment -

          I'm glad the intention is aligned with the desire to have these groovy language features available and understand that support for interface will require work in the groovy CPS.

          In the short term though, is there a recommended workaround that could provide some interface like equivalent? Or is it just not possible to do anything like this at this stage?
          Perhaps pass HashMaps of Closures between the top level "classes" as per
          https://github.com/jenkinsci/workflow-plugin/blob/master/cps-global-lib/README.md#writing-shared-code

          e.g.

          org/foo/Zot.groovy
          package org.foo
          // Closures define the interface for Zot
          Closure show
          Closure name
          
          def specify_interface (Hashmap hm) {
              this.show = hm["show"]
              this.name = hm["name"]
          }
          
          
          flow.groovy
          node {
              def z = new org.foo.Zot()
              z.specify_interface(["show": { echo "Hello" }, "name": { return "Bernard" }])
          
              // you can then pass z around beyond the scope of flow.groovy
              z.show()
              echo z.name()
          }
          

          Or is there a more elegant way within the constraints of what is and what is not possible today?

          Thomas Dalton added a comment - I'm glad the intention is aligned with the desire to have these groovy language features available and understand that support for interface will require work in the groovy CPS. In the short term though, is there a recommended workaround that could provide some interface like equivalent? Or is it just not possible to do anything like this at this stage? Perhaps pass HashMaps of Closures between the top level "classes" as per https://github.com/jenkinsci/workflow-plugin/blob/master/cps-global-lib/README.md#writing-shared-code e.g. org/foo/Zot.groovy package org.foo // Closures define the interface for Zot Closure show Closure name def specify_interface (Hashmap hm) { this .show = hm[ "show" ] this .name = hm[ "name" ] } flow.groovy node { def z = new org.foo.Zot() z.specify_interface([ "show" : { echo "Hello" }, "name" : { return "Bernard" }]) // you can then pass z around beyond the scope of flow.groovy z.show() echo z.name() } Or is there a more elegant way within the constraints of what is and what is not possible today?

          Jesse Glick added a comment -

          I would just try using Java-compatible syntax first. More likely to already be supported.

          Jesse Glick added a comment - I would just try using Java-compatible syntax first. More likely to already be supported.

          Thomas Dalton added a comment -

          Ahh.. in which case I apologies for the miscommunication on my part, I actually tried the Java syntax first - I then reduced the problem to simpler forms to post the issue.

          Here is what I tried originally:

          interface Foo {
              String name()
          }
          class Bar implements Foo {
              def String name() {
                  return "Ronald"
              }
          }
          
          node {
              b = new Bar()
              a = b as Foo
          
              echo a.name()
          }
          

          This doesn't work either and the same exception Cannot invoke method visit() on null object is thrown.

          Yet under a groovy console:

          interface Foo {
              String name()
          }
          
          class Bar implements Foo {
              def String name() {
                  return "Ronald"
              }
          }
          
          b = new Bar()
          a = b as Foo
          
          println a.name()
          

          It works fine. Hence I think it is likely a problem with all uses of interface and why I'm on the lookout for a decent workaround

          Thomas Dalton added a comment - Ahh.. in which case I apologies for the miscommunication on my part, I actually tried the Java syntax first - I then reduced the problem to simpler forms to post the issue. Here is what I tried originally: interface Foo { String name() } class Bar implements Foo { def String name() { return "Ronald" } } node { b = new Bar() a = b as Foo echo a.name() } This doesn't work either and the same exception Cannot invoke method visit() on null object is thrown. Yet under a groovy console: interface Foo { String name() } class Bar implements Foo { def String name() { return "Ronald" } } b = new Bar() a = b as Foo println a.name() It works fine. Hence I think it is likely a problem with all uses of interface and why I'm on the lookout for a decent workaround

          Jesse Glick added a comment -

          Well my first suggestion is to try actual Java syntax:

          Foo a = b;
          

          in case it is the as keyword which causes the problem.

          Jesse Glick added a comment - Well my first suggestion is to try actual Java syntax: Foo a = b; in case it is the as keyword which causes the problem.

          Thomas Dalton added a comment -

          If I replace

          a = b as Foo
          

          with

          Foo a = b
          

          It doesn't work either I'm afraid , same exception.

          Thomas Dalton added a comment - If I replace a = b as Foo with Foo a = b It doesn't work either I'm afraid , same exception.

            kohsuke Kohsuke Kawaguchi
            tomjdalton Thomas Dalton
            Votes:
            4 Vote for this issue
            Watchers:
            10 Start watching this issue

              Created:
              Updated: