I agree with Stephen here; adding methods seems risky and could break
existing plugins, at least in a backwards compat sense.
But I at the same time do like the idea of getting javadoc for the facets
and fragments. So can we get both somehow?
E.g. if there is a @StaplerFacet("login") or @StaplerFragment("main") there
should be a corresponding
@facet login the login page.
@fragment main add this to change the main part of the configuration page.
in the javadoc of the class?
/B
2017-07-06 13:16 GMT+02:00 Stephen Connolly <[email protected]
>:
>
>
> On 5 July 2017 at 18:10, Kohsuke Kawaguchi <[email protected]> wrote:
>
>> FWIW, I've already discussed this offline with Stephen and I'm
>> generally +1.
>>
>> One suggestion I had for him is to consider using a Java method
>> definition instead of @StaplerFacet and @StaplerFragment. That is, instead
>> of this:
>>
>> @StaplerFacets({
>> @StaplerFacet("index"),
>> @StaplerFacet("config")
>> })
>> @StaplerFragments({
>> @StaplerFragment("main"),
>> @StaplerFragment("side-panel"),
>> @StaplerFragment(value="tasks",optional=true)
>> })
>> public class Widget { ... }
>>
>> Consider that:
>>
>> public class Widget {
>> @StaplerFacet protected void index() {}
>> @StaplerFacet protected void config() {}
>>
>> @StaplerFragment protected abstract void main();
>> @StaplerFragment("side-panel") protected abstract void sidepanel();
>> @StaplerFragment protected void tasks();
>> }
>>
>>
> Well at least this is a better explanation than you gave me verbally ;-)
>
>
>> I liked this better because ...
>>
>> - It fits with the original design thinking behind Stapler, which is
>> that views are like methods. Their inheritance and override semantics are
>> all modeled after methods.
>> - It avoids a long list of @StaplerFragment/@StaplerFacet at the top
>> of the class declaration. We have some objects in the core that has quite
>> a
>> few views.
>>
>> So let's see:
>
> $ ( cd core/src/main/resources ; find . -name \*.jelly -exec dirname {} \;
> ; find . -name \*.groovy -exec dirname {} \; ) | sort | uniq -c | grep -v
> ./lib/ | sort -n | tail -n 10
> 7 ./hudson/model/AbstractProject
> 7 ./hudson/slaves/SlaveComputer
> 7 ./jenkins/install/SetupWizard
> 9 ./hudson/model/Run
> 11 ./hudson/PluginManager
> 11 ./hudson/security/HudsonPrivateSecurityRealm
> 12 ./hudson/model/Job
> 13 ./hudson/model/View
> 14 ./hudson/model/Computer
> 25 ./jenkins/model/Jenkins
>
> So there are not altogether too many classes in Jenkins having lots of
> fragments / facets
>
> I think an argument based on a couple of really big classes like Jenkins
> having 25 lines of
>
> @StaplerObject
> @StaplerFacet("_restart")
> @StaplerFacet("_safeRestart")
> @StaplerFacet("_script")
> @StaplerFacet("_scriptText")
> @StaplerFacet("accessDenied")
> @StaplerFacet("configure")
> @StaplerFacet("configureExecutors")
> @StaplerFacet("fingerprintCheck")
> @StaplerFacet("legend")
> @StaplerFacet("load-statistics")
> @StaplerFacet("login")
> @StaplerFacet("loginError")
> @StaplerFacet("manag_")
> @StaplerFacet("manage")
> @StaplerFacet("newView")
> @StaplerFacet("noPrincipal")
> @StaplerFacet("oops")
> @StaplerFacet("opensearch.xml")
> @StaplerFacet("projectRelationship-help")
> @StaplerFacet("projectRelationship")
> @StaplerFacet("systemInfo")
> @StaplerFacet("threadDump")
> @StaplerFragment("_api")
> @StaplerFragment("downgrade")
> @StaplerFragment("sidepanel")
> public class Jenkins {
> ...
> }
>
> compared with
>
> @StaplerObject
> public class Jenkins {
> @StaplerFacet protected void _restart() {}
> @StaplerFacet protected void _safeRestart() {}
> @StaplerFacet protected void _script() {}
> @StaplerFacet protected void _scriptText() {}
> @StaplerFacet protected void accessDenied() {}
> @StaplerFacet protected void configure() {}
> @StaplerFacet protected void configureExecutors() {}
> @StaplerFacet protected void fingerprintCheck() {}
> @StaplerFacet protected void legend() {}
> @StaplerFacet @StaplerPath("load-statistics") protected void
> loadStatistics() {}
> @StaplerFacet protected void login() {}
> @StaplerFacet protected void loginError() {}
> @StaplerFacet protected void manag_() {}
> @StaplerFacet protected void manage() {}
> @StaplerFacet protected void newView() {}
> @StaplerFacet protected void noPrincipal() {}
> @StaplerFacet protected void oops() {}
> @StaplerFacet @StaplerPath("opensearch.xml") protected void
> opensearchXml() {}
> @StaplerFacet @StaplerPath("projectRelationship-help") protected void
> projectRelationshipHelp()
> @StaplerFacet protected void projectRelationship() {}
> @StaplerFacet protected void systemInfo() {}
> @StaplerFacet protected void threadDump() {}
> @StaplerFragment protected void _api() {}
> @StaplerFragment protected void downgrade() {}
> @StaplerFragment protected void sidepanel() {}
> ...
> }
>
> just to get the javadoc comments and leverage abstract and final... I
> really do not like the extra effort of typing in all those method names...
>
> Perhaps the biggest issue for me is that to retrofit that into Jenkins we
> would need to take great care that these methods we are adding as facet and
> fragment markers do not have names that may potentially conflict with
> subclasses...
>
> If we add
>
> @StaplerObject
> public abstract class Descriptor ... {
> ...
> @StaplerFragment protected abstract void config(); // doesn't really
> matter if abstract or not here
> ...
> }
>
> we may make it impossible for a concrete subclass to have a
>
> public MyDescriptor extends Descriptor ... {
>
> private MyConfig config;
>
> ...
>
> public MyConfig config() {
> return config.clone();
> }
>
> }
>
> as they will be unable to retain binary compatibility with their existing
> "config" method when they upgrade...
>
> So that means we will need to do
>
> @StaplerObject
> public abstract class Descriptor ... {
> ...
> @StaplerFragment @StaplerPath("config") protected abstract void
> _retrofit_config();
> ...
> }
>
> Thinking some more, I see issues with plugin authors being thereby forced
> to add implementations when they upgrade core... ok so they are quick and
> the IDE will help... but having to add all those
>
> public MyDescriptor extends Descriptor ... {
>
> private MyConfig config;
>
> ...
>
> public MyConfig config() {
> return config.clone();
> }
>
> @StaplerFragment @StaplerPath("config") protected void
> _retrofit_config() {}
> }
>
> All the while hoping that the IDE has copied the method annotations
> (otherwise the method is not annotated) and the plugin author has not opted
> into @StaplerObject yet
>
> That, to me, does not make for a compelling developer experience.
>
> WDYT?
>
> I am inclined to reject your method based idea and stick with class level
> annotations... there are only 11 classes in all of Jenkins core with more
> than 5 facets / fragments
>
>
>> - It creates a nice to place to describe view as javadoc.
>> - 'abstract' and 'final' provides convenient semantics and javac does
>> that work for us. One less thing to do for our annotation processor
>>
>> Obvious downside is that those are not real methods.
>>
>> On Mon, Jul 3, 2017 at 5:21 AM Stephen Connolly <
>> [email protected]> wrote:
>>
>>> I have been developing Jenkins plugins and changes to Jenkins core for
>>> more than 10 years now. As such, I have internalized a lot of the knowledge
>>> about how to develop against Jenkins and more specifically against Stapler.
>>>
>>> When I talk to people trying to start out development against Jenkins,
>>> the magic of Stapler seems to be a big source of confusion - at least to
>>> the people I have talked to.
>>>
>>> Prospective developers get confused:
>>>
>>> - between primary facets and facet fragments.
>>> - where to put facets and facet fragments.
>>> - which facet fragments are optional and which ones are mandatory
>>>
>>> (Did you even know that those jelly / groovy views are called facets and
>>> that there are two kinds?)
>>>
>>> Some of those concerns affect me too... but I am a seasoned Jenkins
>>> developer CMD+SHIFT+P, *, ENTER and IntelliJ is showing me a full list of
>>> facets and facet fragments from the class hierarchy and I can search each
>>> one looking for the <st:include page="fragment" optional="true"> to
>>> discover that there is an optional facet fragment... and then try and dig
>>> through the Jelly / Groovy logic to determine when and why that fragment
>>> should be included.
>>>
>>> It doesn't just stop there... There are those methods that we expect
>>> Stapler to invoke for us... typically they are the doFillXYZItems() or
>>> doCheckXYZ() methods. I find myself having to mark them up like:
>>>
>>> @SuppressWarnings("unused") // stapler
>>> public FormValidation doCheckWidgetValue(@QueryParameter String value) {
>>> ...
>>> }
>>>
>>>
>>> So that my IDE stops complaining about the dead code. It's not a big
>>> deal in the grand scheme of things, but I'd much rather have an annotation
>>> on the method to signify that we expect the method to be used by stapler...
>>> Right now I could do that using
>>>
>>> @WebMethod(name="checkWidgetValue")
>>> public FormValidation doCheckWidgetValue(@QueryParameter String value) {
>>> ...
>>> }
>>>
>>>
>>> If I don't like that, I guess I could use the newer HTTP verb
>>> annotations in stapler:
>>>
>>> @GET // stapler
>>> public FormValidation doCheckWidgetValue(@QueryParameter String value) {
>>> ...
>>> }
>>>
>>>
>>> But the simple @GET is not screaming out stapler to me, so I feel
>>> compelled to add a // stapler comment to remind myself that the
>>> annotation is used by stapler
>>>
>>> Then we hit the actual name that stapler exposes things on. There are
>>> some conventions that stapler follows... and they are magic conventions...
>>> so much so that I just keep http://stapler.kohsuke.org/reference.html
>>> as the first bookmark in my bookmark bar.
>>>
>>> I think we can do better. I think we should do better, and here is my
>>> proposal.
>>>
>>> We should introduce some annotations. Make them available against older
>>> versions of Jenkins so that we don't have to wait for everyone to update
>>> their plugin baseline. The annotations can be added as a plugin dependency
>>> in the parent pom, that everyone can use them just by upgrading the plugin
>>> parent pom.
>>>
>>> Here are the class annotations I think we need:
>>>
>>> - An annotation (I propose @StaplerObject) that says "This object is
>>> expected to bound to a URL by Stapler, the Stapler annotations are
>>> complete, check that there are no facets / fragments without a
>>> corresponding annotation and no magic on this class please" (so if there
>>> is, say a rename.jelly but no @StaplerFacet("rename") or
>>> @StaplerFragment("rename") then you get an error)
>>> - An annotation (I propose @StaplerFacet and the repeatable
>>> container @StaplerFacets) that says "Here are the facets that this
>>> object is expected to have" (doesn't have to be on a @StaplerObject
>>> but when not on a @StaplerObject we can only check for missing
>>> expected facets, we cannot check for "unexpected" facets - i.e. I
>>> created a
>>> facet with a spelling mistake or typo in the name)
>>> - An annotation (I propose @StaplerFragment and the repeatable
>>> container @StaplerFragments) that says "Here are the facet fragments
>>> that this object is required or may optionally have" (quite often will
>>> not
>>> be on a @StaplerObject)
>>>
>>>
>>> So I envision something like this (pre-Java 8):
>>>
>>> @StaplerObject
>>> @StaplerFacets({
>>> @StaplerFacet("index"),
>>> @StaplerFacet("config")
>>> })
>>> @StaplerFragments({
>>> @StaplerFragment("main"),
>>> @StaplerFragment("side-panel"),
>>> @StaplerFragment(value="tasks",optional=true)
>>> })
>>> public class Widget { ... }
>>>
>>>
>>> When compiled targeting Java 8 this would become:
>>>
>>> @StaplerObject
>>> @StaplerFacet("index")
>>> @StaplerFacet("config")
>>> @StaplerFragment("main")
>>> @StaplerFragment("side-panel")
>>> @StaplerFragment(value="tasks",optional=true)
>>> public class Widget { ... }
>>>
>>>
>>> There would be an annotation processor that could do some checks:
>>>
>>> - If you add @StaplerFragment(..., optional=false) then any
>>> non-abstract class must have that fragment somewhere in the class
>>> hierarchy
>>> - If you add @StaplerFacet then any non-abstract class must have
>>> that fragment somewhere in the class hierarchy
>>> - If you add @StaplerObject then only the named facets and facet
>>> fragments must be present for that class (to catch somebody calling the
>>> facet resource with an incorrect name - it should be
>>> side-panel2.jelly but they called it sidepanel2.jelly)
>>>
>>>
>>> Then, while we are at it, I'd also like to add method / field
>>> annotations:
>>>
>>> - An annotation (I propose @StaplerPath and the repeatable container
>>> @StaplerPaths) that says "here is the URL segment names that this
>>> method / field matches when Stapler is binding the URL". This annotation
>>> can be used to enable easier refactoring without risk of breaking the
>>> URLs
>>> because we can rename the getters and keep the URL scheme as before or
>>> even
>>> keep the legacy URLs but direct to the new URLs via the UI.
>>> - Annotations (I propose @StaplerHEAD / @StaplerGET / @StaplerPOST /
>>> @StaplerPUT / @StaplerPATCH / @StaplerDELETE plus non standard
>>> methods supported with @StaplerMethod("...") and then a repeatable
>>> container of @StaplerMethods - the repeatable container would only
>>> take @StaplerMethod annotations though) that cover the different
>>> HTTP methods (a.k.a. verbs) - we have some existing, but they need some
>>> extensions and I think it might be a good idea to consolidate the names,
>>> especially if we want to allow consumption on older baseline versions of
>>> Jenkins
>>>
>>>
>>> So this could be something like this (Pre Java 8):
>>>
>>> public class Widget {
>>> @StaplerPath
>>> public Manchu manchu;
>>>
>>> @StaplerPaths({@StaplerPath,@StaplerPath("fu")})
>>> public Foo foo;
>>>
>>> @StaplerPath
>>> public Bar getBar() { ... }
>>>
>>> @StaplerPath(StaplerPath.DYNAMIC)
>>> public Object getDynamic(StaplerRequest req) { ... }
>>>
>>> @StaplerPOST
>>> public HttpResponse doActivate(StaplerRequest req) { ... }
>>>
>>> @StaplerPath(StaplerPath.DYNAMIC)
>>> @StaplerPUT
>>> public HttpResponse doDynamic(StaplerRequest req) { ... }
>>> }
>>>
>>>
>>> Or targeting Java 8:
>>>
>>> public class Widget {
>>> @StaplerPath
>>> public Manchu manchu;
>>>
>>> @StaplerPath
>>> @StaplerPath("fu")
>>> public Foo foo;
>>>
>>> @StaplerPath
>>> public Bar getBar() { ... }
>>>
>>> @StaplerPath(StaplerPath.DYNAMIC)
>>> public Object getDynamic(StaplerRequest req) { ... }
>>>
>>> @StaplerPOST
>>> public HttpResponse doActivate(StaplerRequest req) { ... }
>>>
>>> @StaplerPath(StaplerPath.DYNAMIC)
>>> @StaplerPUT
>>> public HttpResponse doDynamic(StaplerRequest req) { ... }
>>> }
>>>
>>>
>>> The annotation processor can then give additional checks:
>>>
>>>
>>> - @StaplerPath annotated fields must be public
>>> - @StaplerPath(altName) annotated fields are only allowed if
>>> targeting a new version Stapler that adds support for that (unless the
>>> name
>>> is the inferred name)
>>> - @StaplerPath annotated methods must be public and either match one
>>> of the allowed getter signatures or also have a HTTP method annotation
>>> and
>>> match the allowed action method signatures.
>>> - @StaplerPath(altName) annotated methods are only allowed if
>>> targeting a new version Stapler that adds support for that (unless the
>>> name
>>> is the inferred name)
>>> - @StaplerHEAD / @StaplerGET / @StaplerPOST / @StaplerPUT /
>>> @StaplerPATCH / @StaplerDELETE annotated methods must be public and
>>> match one of the allowed action method signatures. The method name must
>>> match the convention for action method inference (subject to
>>> @WebMethod(name="...") overriding when *compiling against older
>>> versions of Stapler* - but it would be an error to use @WebMethod if
>>> compiling against newer versions of Stapler because we can move to the
>>> new
>>> annotations in those cases)
>>> - If the class is @StaplerObject annotated we can do additional
>>> checks for overlapping facets, for example where an action method or a
>>> getter will hide a facet (it's ok to hide the facet for non HEAD/GET
>>> requests though)
>>>
>>> There are probably additional checks that we can add as we figure them
>>> out.
>>>
>>>
>>> Now there are some open questions:
>>>
>>> - Does it make sense to start all these annotations with Stapler? My
>>> initial stab at this was to use @Staple in place of @StaplerPath and
>>> to re-use the @GET / @POST / @PUT / @DELETE verbs but mandating the
>>> @Staple annotation so that you would have:
>>>
>>> @Staple @POST
>>>
>>> public HttpResponse doActivate(StaplerRequest req) { ... }
>>>
>>>
>>> rather than
>>>
>>> @StaplerPOST
>>>
>>> public HttpResponse doActivate(StaplerRequest req) { ... }
>>>
>>>
>>> One of the issues I found with that is class conflicts when back-porting
>>> the @GET / @POST / etc annotations, so we could only use those if we
>>> were prepared to mark 2.7 as the oldest core that could use the
>>> annotations, whereas we can make new annotations in a separate package
>>> available as far back as Stapler 1.180 == Jenkins 1.455 (or worst case
>>> Stapler 1.237 == Jenkins 1.651)
>>>
>>> Now we could introduce new annotations without the @Stapler prefix, but
>>> then your code complete would confuse you between
>>> @org.kohsuke.stapler.annotations.GET and @org.kohsuke.stapler.verbs.GET
>>> plus we still need something to give a hint that the annotation is implying
>>> Stapler's involvement (at least from my perspective)
>>>
>>>
>>> - I had thought about saying that the class level @StaplerObject
>>> should be enough of a flag that you should expect the other
>>> annotations...
>>> but the class annotation will be inherited, so you may not see it until
>>> you
>>> look at the super-class... and @Path will conflict with
>>> java.nio.file.Path... that might work with @Staple (and @Staples as
>>> the container) but while cute they do not scream to the new developers
>>> that
>>> these are involved in url binding.
>>>
>>> NOTE: In this context I see this change as being entirely opt-in. Plugin
>>> developers should not have to go adding the annotations - though we would
>>> probably apply them in Jenkins core to assist new developers.
>>>
>>>
>>> - If we go with the Stapler prefix, would it make sense to
>>> consolidate all the annotations with that prefix?
>>>
>>> - I am not convinced for the case of @ExportedBean and @Exported,
>>> these reflect a different cross-cutting concern and as such the pair
>>> seem
>>> named well from my PoV
>>> - The @JavaScriptMethod / @WithWellKnownURL annotations might be
>>> worthy of consolidation if we can determine better names.
>>> - The parameter binding annotations such as @AncestorInPath,
>>> @Header, @QueryParameter and @InjectedParameter I think are fine
>>> where they are as they will only be on an action method which already
>>> has
>>> the @StaplerPOST etc indicator.
>>> - The meta-annotation @InterceptorAnnotation should not be
>>> consolidated, in any case we could not consolidate it and retain usage
>>> against older cores
>>> - The other annotations are likely rarely used, but if we can
>>> find good names and people think they are generically useful rather
>>> than
>>> single use-case hacks added into the stapler API, I think
>>> consolidation
>>> might make the features more widely used.
>>>
>>> So, over to you the community of Jenkins developers:
>>>
>>> 1. Would this change have helped you get started quicker?
>>> 2. Would this change help you even now?
>>> 3. How ugly are my proposed annotation names? Can you provide a less
>>> ugly scheme of names?
>>> 4. Anything else?
>>>
>>> Stephen
>>>
>>> --
>>> You received this message because you are subscribed to the Google
>>> Groups "Jenkins Developers" group.
>>> To unsubscribe from this group and stop receiving emails from it, send
>>> an email to [email protected].
>>> To view this discussion on the web visit https://groups.google.com/d/ms
>>> gid/jenkinsci-dev/CA%2BnPnMz-m49TK7Em%2BxBNb%2BV98dBCz9CrrP
>>> Xg3uW6%2B_x3KX5gOQ%40mail.gmail.com
>>> <https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMz-m49TK7Em%2BxBNb%2BV98dBCz9CrrPXg3uW6%2B_x3KX5gOQ%40mail.gmail.com?utm_medium=email&utm_source=footer>
>>> .
>>> For more options, visit https://groups.google.com/d/optout.
>>>
>> --
>> Kohsuke Kawaguchi
>>
>> --
>> You received this message because you are subscribed to the Google Groups
>> "Jenkins Developers" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to [email protected].
>> To view this discussion on the web visit https://groups.google.com/d/ms
>> gid/jenkinsci-dev/CAN4CQ4wDS34DE-PQVK6tsCQD5AXWcxCMEVkCgh5MN
>> ymRNVcLPw%40mail.gmail.com
>> <https://groups.google.com/d/msgid/jenkinsci-dev/CAN4CQ4wDS34DE-PQVK6tsCQD5AXWcxCMEVkCgh5MNymRNVcLPw%40mail.gmail.com?utm_medium=email&utm_source=footer>
>> .
>>
>> For more options, visit https://groups.google.com/d/optout.
>>
>
> --
> You received this message because you are subscribed to the Google Groups
> "Jenkins Developers" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected].
> To view this discussion on the web visit https://groups.google.com/d/
> msgid/jenkinsci-dev/CA%2BnPnMwio9PeC3uhC_i4XqQ_7OY2dN2Hx34e6hSmHVAeG7RiRA%
> 40mail.gmail.com
> <https://groups.google.com/d/msgid/jenkinsci-dev/CA%2BnPnMwio9PeC3uhC_i4XqQ_7OY2dN2Hx34e6hSmHVAeG7RiRA%40mail.gmail.com?utm_medium=email&utm_source=footer>
> .
>
> For more options, visit https://groups.google.com/d/optout.
>
--
Robert Sandell
*Software Engineer*
*CloudBees Inc.*
--
You received this message because you are subscribed to the Google Groups
"Jenkins Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/jenkinsci-dev/CALzHZS10%2BzMD58BLJBiksxY7ch7sDQ7rPQEN9y-SJACGhREoTA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.