First, I want to say that I appreciate the work you are doing to make ShrinkWrap Descriptors robust and complete, Ralf. Prior to your involvement, descriptors was stuck in prototype land. I'm looking forward to when it becomes a cornerstone of Arquillian tests.
I have some feedback that I'd like to share to help us find the right path.
My first impression of the 1.2.0 API was nearly identical to what Bartosz shared. It was the first time I had used the API since the original prototype and I just didn't feel good about how it was coming out after the refactoring.
Here are three examples with the before and after, which I'll use to point out a few shortcomings.
Before #1
.setWebXML(new StringAsset(Descriptors.create(WebAppDescriptor.class)
.envEntry("name", String.class, "infinispan.xml").exportAsString()));
After #1
.setWebXML(new StringAsset(Descriptors.create(WebAppDescriptor.class)
.getOrCreateEnvEntry().envEntryName("name").envEntryType("java.lang.String").envEntryValue("infinispan.xml").up()
.exportAsString());
Before #2
.setWebXML(new StringAsset(Descriptors.create(WebAppDescriptor.class)
.servlet(EchoServlet.class, EchoServlet.URL_PATTERN).exportAsString()));
After #2
.setWebXML(new StringAsset(Descriptors.create(WebAppDescriptor.class)
.createServlet()
.servletName(EchoServlet.class.getSimpleName())
.servletClass(EchoServlet.class.getName()).up()
.createServletMapping()
.servletName(EchoServlet.class.getSimpleName())
.urlPattern(EchoServlet.URL_PATTERN).up()
.exportAsString()));
Before #3
.addAsWebInfResource(new StringAsset(Descriptors.create(BeansDescriptor.class)
.alternativeClass(MockPaymentProcessor.class).exportAsString())
After #3
.addAsWebInfResource(new StringAsset(Descriptors.create(BeansDescriptor.class)
.createAlternatives().clazz(MockPaymentProcessor.class.getName()).up()
.exportAsString())
up() method:
I want to start off by saying that I don't like the up() method at all. I think that's an indication of a fluent API that isn't yet all there, and it's very awkward (see #1, #2 and #3). The consumer of the API should not be aware of the internal traversal that is needed to step out of working on the current node. If you begin an operation on another node, the nesting level change should be implicit.
If there is absolutely no way to avoid this method, then I propose we at least give it a friendlier name, such as closeElement(), endElement() or simply close().
package names:
We've learned from naming the containers that version numbers should never be condensed in a package or artifact name since it causes ambiguity and it's not future proof. Here's an example of one of these packages today:
org.jboss.shrinkwrap.descriptor.api.beans10.BeansDescriptor
This package should instead be:
org.jboss.shrinkwrap.descriptor.api.beans_1_0.BeansDescriptor
It's longer, but it's clear and safe. The consumer isn't left stratching their heading thinking 10?
class names as strings:
Whenever a full-qualified class name is needed, the method should be overloaded to accept either a class reference or a full-qualified class name string, consistent w/ ShrinkWrap archives. In SWD 1.2.0, a string is the only option. Not only do we lose the chance at type safety, it also increases code verbosity. (see #2 and #3)
The same goes for parameters that are types. Even if a "type" could be a generic reference, if a class reference is possible, SWD should have a method that accomodates that parameter format. (see envEntryType() in #1).
There's a similar problem with enums as values. I think when a value is accepted, the method signature should be changed to Object and then toString() called on it before accepting the value. For example, this change would accomodate: paramValue(ProjectStage.Development).
getOrCreate*
The getOrCreate* method sounds like a method that's not sure what it wants to do. To some, this combination of behavior is a code smell. I get the intent, but we can make this look a lot cleaner. Here is one idea:
contextParam().paramName(ProjectStage.PROJECT_STAGE_PARAM_NAME).paramValue(ProjectStage.Development.name()).save()
The save() method (or alternate name) at the end of the chain would try to find an existing parameter with the same key (meaning however an element is uniquely defined). If an element is found, it updates that element. If the element is not found, a new one is created and inserted. We've also changed the purpose of the getOrCreate* method to merely descend us into the context of the element (lazy traversal).
If two methods are necessary, we can have "save()" / "define()" (add but fails if there is an existing element) and "update()" (which adds or creates)
Associated elements
I know this one is tricky, but you can see in #2 that the servlet and servlet mappings now have to be lined up on the servlet name as the key. This might be a valid case when we need to "giftwrap" the ShrinkWrap Descriptor API to offer a convenience method that treats the definition of these two elements as a single chain (the fact that they are represented as two elements is really just an implemention detail (or quirk).
exportAsString()
Although it's always been around, the need to exportAsString(), and even StringAsset() seems like a leaky abstraction. I'd really like to see support in either ShrinkWrap Archives, SWD or an integration library that can convert a Descriptor into an Asset. To be honest, I don't see why we can't have:
Descriptors.create(BeanDescriptor.class).asAsset()
(maybe move Asset to ShrinkWrap Core if necessary) (and split off ShrinkWrap Archives).
Wrap-up
I know it's not easy to go from an XSD to the type of customizations I'm talking about. However, if we have transformers at generation time that can but some of this logic in place for all descriptors, or can offer consolidated functionality for things like defining a Servlet and Servlet mapping in a single logical element.
Let's not forget the consumer of the API. I think there is plenty of room to offer them something friendlier than what we have in 1.2.0. We'll get it!